diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..416c47b1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +ereuse_devicehub/static/vendor +ereuse_devicehub/static/js/print.pdf.js +ereuse_devicehub/static/js/qrcode.js +*.min.js \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..56f4296d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "env": { + "browser": true, + "es2021": true, + "jquery": true + }, + "extends": [ + "airbnb", + "prettier" + ], + "plugins": [ + "prettier" + ], + "parserOptions": { + "ecmaVersion": "latest" + }, + "rules": { + "quotes": ["error","double"], + "no-use-before-define": "off", + "no-unused-vars": "warn", + "no-undef": "warn", + "camelcase": "off", + "no-console": "off", + "no-plusplus": "off", + "no-param-reassign": "off", + "no-new": "warn", + "strict": "off", + "class-methods-use-this": "off", + "eqeqeq": "warn", + "radix": "warn", + "max-classes-per-file": ["error", 2] + }, + "globals": { + "API_URLS": true, + "Api": true + } +} \ No newline at end of file diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 00000000..7e42feaf --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,55 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# ESLint is a tool for identifying and reporting on patterns +# found in ECMAScript/JavaScript code. +# More details at https://github.com/eslint/eslint +# and https://eslint.org + +name: ESLint + +on: + push: + branches: [master, testing] + pull_request_target: + branches: [master, testing] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: '16' + - name: Install dependencies + run: npm install + - name: Run linters + uses: wearerequired/lint-action@v1 + with: + eslint: true + prettier: false + commit_message: "Fix code style issues with ${linter}" + auto_fix: true + commit: true + github_token: "${{ secrets.GITHUB_TOKEN }}" + git_name: "Lint Action" + - name: Save Code Linting Report JSON + # npm script for ESLint + # eslint --output-file eslint_report.json --format json src + # See https://eslint.org/docs/user-guide/command-line-interface#options + run: npm run lint:report + # Continue to the next step even if this fails + continue-on-error: true + - name: Annotate Code Linting Results + uses: ataylorme/eslint-annotate-action@1.2.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + report-json: "eslint_report.json" + only-pr-files: true + - name: Upload ESLint report + uses: actions/upload-artifact@v2 + with: + name: eslint_report.json + path: eslint_report.json diff --git a/.gitignore b/.gitignore index 6a130a6e..0d6f75c3 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,11 @@ ENV/ # Temporal dir tmp/ + +# NPM modules +node_modules/ +yarn.lock + +# ESLint Report +eslint_report.json + diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..f92e93ef --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 250 +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4a03f1..35d175a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,30 @@ ml). ## testing - [added] #219 Add functionality to searchbar (Lots and devices). +- [added] #222 Allow user to update its password. +- [added] #233 Filter in out trades from lots selector. +- [added] #236 Allow select multiple devices in multiple pages. +- [added] #237 Confirmation dialog on apply lots changes. +- [added] #238 Customize labels. +- [added] #242 Add icons in list of devices. +- [added] #244 Select full devices. +- [added] #257 Add functionality to search generic categories like all components. +- [added] #252 new tabs lots and public link in details of one device. - [changed] #211 Print DHID-QR label for selected devices. - [changed] #218 Add reactivity to device lots. +- [changed] #220 Add reactive lots list. +- [changed] #232 Set max lots list to 20. +- [changed] #235 Hide trade buttons. +- [changed] #239 Change Tags for Unique Identifier. +- [changed] #247 Change colors. +- [changed] #253 Drop download public links. - [fixed] #214 Login workflow +- [fixed] #221 Fix responsive issues on frontend. +- [fixed] #223 fix trade lots modal. +- [fixed] #224 fix clickable lots selector not working when click in text. +- [fixed] #254 Fix minor types in frontend. +- [fixed] #255 Fix status column on device list. + ## [2.0.0] - 2022-03-15 First server render HTML version. Completely rewrites views of angular JS client on flask. diff --git a/README.md b/README.md index 37c85957..5c7019cf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Devicehub +# Devicehub Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org) 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/api/views.py b/ereuse_devicehub/api/views.py index 7a894394..ff5bd014 100644 --- a/ereuse_devicehub/api/views.py +++ b/ereuse_devicehub/api/views.py @@ -5,7 +5,8 @@ from flask import Blueprint from flask import current_app as app from flask import g, jsonify, request from flask.views import View -from marshmallow import ValidationError +from flask.wrappers import Response +from marshmallow.exceptions import ValidationError from werkzeug.exceptions import Unauthorized from ereuse_devicehub.auth import Auth @@ -51,18 +52,22 @@ class InventoryView(LoginMix, SnapshotMix): self.tmp_snapshots = app.config['TMP_SNAPSHOTS'] self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email) snapshot_json = self.validate(snapshot_json) - try: - self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot() - except ValidationError: - self.response = jsonify('') - self.response.status_code = 201 - return self.response + if type(snapshot_json) == Response: + return snapshot_json + + self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot() snapshot = self.build() db.session.add(snapshot) db.session().final_flush() db.session.commit() - self.response = self.schema.jsonify(snapshot) + self.response = jsonify( + { + 'url': snapshot.device.url.to_text(), + 'dhid': snapshot.device.devicehub_id, + 'sid': snapshot.sid, + } + ) self.response.status_code = 201 move_json(self.tmp_snapshots, self.path_snapshot, g.user.email) return self.response @@ -74,11 +79,15 @@ class InventoryView(LoginMix, SnapshotMix): except ValidationError as err: txt = "{}".format(err) uuid = snapshot_json.get('uuid') + sid = snapshot_json.get('sid') error = SnapshotErrors( - description=txt, snapshot_uuid=uuid, severity=Severity.Error + description=txt, snapshot_uuid=uuid, severity=Severity.Error, sid=sid ) error.save(commit=True) - raise err + # raise err + self.response = jsonify(err) + self.response.status_code = 400 + return self.response api.add_url_rule('/inventory/', view_func=InventoryView.as_view('inventory')) diff --git a/ereuse_devicehub/client.py b/ereuse_devicehub/client.py index 3224dfd3..92aa67d2 100644 --- a/ereuse_devicehub/client.py +++ b/ereuse_devicehub/client.py @@ -2,7 +2,10 @@ from inspect import isclass from typing import Dict, Iterable, Type, Union from ereuse_utils.test import JSON, Res -from teal.client import Client as TealClient, Query, Status +from flask.testing import FlaskClient +from flask_wtf.csrf import generate_csrf +from teal.client import Client as TealClient +from teal.client import Query, Status from werkzeug.exceptions import HTTPException from ereuse_devicehub.resources import models, schemas @@ -13,110 +16,156 @@ ResourceLike = Union[Type[Union[models.Thing, schemas.Thing]], str] class Client(TealClient): """A client suited for Devicehub main usage.""" - def __init__(self, application, - response_wrapper=None, - use_cookies=False, - allow_subdomain_redirects=False): - super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects) + def __init__( + self, + application, + response_wrapper=None, + use_cookies=False, + allow_subdomain_redirects=False, + ): + super().__init__( + application, response_wrapper, use_cookies, allow_subdomain_redirects + ) - def open(self, - uri: str, - res: ResourceLike = None, - status: Status = 200, - query: Query = tuple(), - accept=JSON, - content_type=JSON, - item=None, - headers: dict = None, - token: str = None, - **kw) -> Res: + def open( + self, + uri: str, + res: ResourceLike = None, + status: Status = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + item=None, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)): res = res.t - return super().open(uri, res, status, query, accept, content_type, item, headers, token, - **kw) + return super().open( + uri, res, status, query, accept, content_type, item, headers, token, **kw + ) - def get(self, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - status: Status = 200, - item: Union[int, str] = None, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: + def get( + self, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + status: Status = 200, + item: Union[int, str] = None, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: return super().get(uri, res, query, status, item, accept, headers, token, **kw) - def post(self, - data: str or dict, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - status: Status = 201, - content_type: str = JSON, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().post(data, uri, res, query, status, content_type, accept, headers, token, - **kw) + def post( + self, + data: str or dict, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + status: Status = 201, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().post( + data, uri, res, query, status, content_type, accept, headers, token, **kw + ) - def patch(self, - data: str or dict, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - item: Union[int, str] = None, - status: Status = 200, - content_type: str = JSON, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().patch(data, uri, res, query, item, status, content_type, accept, token, - headers, **kw) + def patch( + self, + data: str or dict, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + item: Union[int, str] = None, + status: Status = 200, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().patch( + data, + uri, + res, + query, + item, + status, + content_type, + accept, + token, + headers, + **kw, + ) - def put(self, - data: str or dict, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - item: Union[int, str] = None, - status: Status = 201, - content_type: str = JSON, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().put(data, uri, res, query, item, status, content_type, accept, token, - headers, **kw) + def put( + self, + data: str or dict, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + item: Union[int, str] = None, + status: Status = 201, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().put( + data, + uri, + res, + query, + item, + status, + content_type, + accept, + token, + headers, + **kw, + ) - def delete(self, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - status: Status = 204, - item: Union[int, str] = None, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().delete(uri, res, query, status, item, accept, headers, token, **kw) + def delete( + self, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + status: Status = 204, + item: Union[int, str] = None, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().delete( + uri, res, query, status, item, accept, headers, token, **kw + ) def login(self, email: str, password: str): assert isinstance(email, str) assert isinstance(password, str) - return self.post({'email': email, 'password': password}, '/users/login/', status=200) + return self.post( + {'email': email, 'password': password}, '/users/login/', status=200 + ) - def get_many(self, - res: ResourceLike, - resources: Iterable[Union[dict, int]], - key: str = None, - **kw) -> Iterable[Union[Dict[str, object], str]]: + def get_many( + self, + res: ResourceLike, + resources: Iterable[Union[dict, int]], + key: str = None, + **kw, + ) -> Iterable[Union[Dict[str, object], str]]: """Like :meth:`.get` but with many resources.""" return ( - self.get(res=res, item=r[key] if key else r, **kw)[0] - for r in resources + self.get(res=res, item=r[key] if key else r, **kw)[0] for r in resources ) @@ -126,33 +175,119 @@ class UserClient(Client): It will automatically perform login on the first request. """ - def __init__(self, application, - email: str, - password: str, - response_wrapper=None, - use_cookies=False, - allow_subdomain_redirects=False): - super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects) + def __init__( + self, + application, + email: str, + password: str, + response_wrapper=None, + use_cookies=False, + allow_subdomain_redirects=False, + ): + super().__init__( + application, response_wrapper, use_cookies, allow_subdomain_redirects + ) self.email = email # type: str self.password = password # type: str self.user = None # type: dict - def open(self, - uri: str, - res: ResourceLike = None, - status: int or HTTPException = 200, - query: Query = tuple(), - accept=JSON, - content_type=JSON, - item=None, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().open(uri, res, status, query, accept, content_type, item, headers, - self.user['token'] if self.user else token, **kw) + def open( + self, + uri: str, + res: ResourceLike = None, + status: int or HTTPException = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + item=None, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().open( + uri, + res, + status, + query, + accept, + content_type, + item, + headers, + self.user['token'] if self.user else token, + **kw, + ) # noinspection PyMethodOverriding def login(self): response = super().login(self.email, self.password) self.user = response[0] return response + + +class UserClientFlask: + def __init__( + self, + application, + email: str, + password: str, + response_wrapper=None, + use_cookies=True, + follow_redirects=True, + ): + self.email = email + self.password = password + self.follow_redirects = follow_redirects + self.user = None + + self.client = FlaskClient(application, use_cookies=use_cookies) + self.client.get('/login/') + + data = { + 'email': email, + 'password': password, + 'csrf_token': generate_csrf(), + } + body, status, headers = self.client.post( + '/login/', data=data, follow_redirects=True + ) + self.headers = headers + body = next(body).decode("utf-8") + assert "Unassigned" in body + + def get( + self, + uri='', + data=None, + follow_redirects=True, + content_type='text/html; charset=utf-8', + decode=True, + **kw, + ): + + body, status, headers = self.client.get( + uri, data=data, follow_redirects=follow_redirects, headers=self.headers + ) + if decode: + body = next(body).decode("utf-8") + return (body, status) + + def post( + self, + uri='', + data=None, + follow_redirects=True, + content_type='application/x-www-form-urlencoded', + decode=True, + **kw, + ): + + body, status, headers = self.client.post( + uri, + data=data, + follow_redirects=follow_redirects, + headers=self.headers, + content_type=content_type, + ) + if decode: + body = next(body).decode("utf-8") + return (body, status) diff --git a/ereuse_devicehub/forms.py b/ereuse_devicehub/forms.py index d88c9cf1..0f4cefbe 100644 --- a/ereuse_devicehub/forms.py +++ b/ereuse_devicehub/forms.py @@ -1,7 +1,9 @@ +from flask import g from flask_wtf import FlaskForm from werkzeug.security import generate_password_hash from wtforms import BooleanField, EmailField, PasswordField, validators +from ereuse_devicehub.db import db from ereuse_devicehub.resources.user.models import User @@ -59,3 +61,43 @@ class LoginForm(FlaskForm): self.form_errors.append(self.error_messages['inactive']) return user.is_active + + +class PasswordForm(FlaskForm): + password = PasswordField( + 'Current Password', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + ) + newpassword = PasswordField( + 'New Password', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + ) + renewpassword = PasswordField( + 'Re-enter New Password', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + ) + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + if not g.user.check_password(self.password.data): + return False + + if self.newpassword.data != self.renewpassword.data: + return False + + return True + + def save(self, commit=True): + g.user.password = self.newpassword.data + + db.session.add(g.user) + if commit: + db.session.commit() + return diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 58940460..a4e7ae40 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -58,16 +58,30 @@ from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.user.models import User DEVICES = { - "All": ["All"], + "All": ["All Devices", "All Components"], "Computer": [ + "All Computers", "Desktop", "Laptop", "Server", ], - "Monitor": ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"], - "Mobile, tablet & smartphone": ["Mobile", "Tablet", "Smartphone", "Cellphone"], - "DataStorage": ["HardDrive", "SolidStateDrive"], + "Monitor": [ + "All Monitors", + "ComputerMonitor", + "Monitor", + "TelevisionSet", + "Projector", + ], + "Mobile, tablet & smartphone": [ + "All Mobile", + "Mobile", + "Tablet", + "Smartphone", + "Cellphone", + ], + "DataStorage": ["All DataStorage", "HardDrive", "SolidStateDrive"], "Accessories & Peripherals": [ + "All Peripherals", "GraphicCard", "Motherboard", "NetworkAdapter", @@ -81,26 +95,104 @@ DEVICES = { ], } +COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer'] + +COMPONENTS = [ + 'GraphicCard', + 'DataStorage', + 'HardDrive', + 'DataStorage', + 'SolidStateDrive', + 'Motherboard', + 'NetworkAdapter', + 'Processor', + 'RamModule', + 'SoundCard', + 'Display', + 'Battery', + 'Camera', +] + +MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"] +MOBILE = ["Mobile", "Tablet", "Smartphone", "Cellphone"] +DATASTORAGE = ["HardDrive", "SolidStateDrive"] +PERIPHERALS = [ + "GraphicCard", + "Motherboard", + "NetworkAdapter", + "Processor", + "RamModule", + "SoundCard", + "Battery", + "Keyboard", + "Mouse", + "MemoryCardReader", +] + class FilterForm(FlaskForm): filter = SelectField( - '', choices=DEVICES, default="Computer", render_kw={'class': "form-select"} + '', choices=DEVICES, default="All Computers", render_kw={'class': "form-select"} ) - def __init__(self, *args, **kwargs): + def __init__(self, lots, lot_id, *args, **kwargs): super().__init__(*args, **kwargs) + self.lots = lots + self.lot_id = lot_id + self._get_types() + + def _get_types(self): types_of_devices = [item for sublist in DEVICES.values() for item in sublist] dev = request.args.get('filter') - self.device = dev if dev in types_of_devices else None - if self.device: - self.filter.data = self.device + self.device_type = dev if dev in types_of_devices else None + if self.device_type: + self.filter.data = self.device_type + + def filter_from_lots(self): + if self.lot_id: + self.lot = self.lots.filter(Lot.id == self.lot_id).one() + 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 + ) def search(self): + self.filter_from_lots() + filter_type = None + if self.device_type: + filter_type = [self.device_type] + else: + # Case without Filter + filter_type = COMPUTERS - if self.device: - return [self.device] + # Generic Filters + if "All Devices" == self.device_type: + filter_type = COMPUTERS + ["Monitor"] + MOBILE - return ['Desktop', 'Laptop', 'Server'] + elif "All Components" == self.device_type: + filter_type = COMPONENTS + + elif "All Computers" == self.device_type: + filter_type = COMPUTERS + + elif "All Monitors" == self.device_type: + filter_type = MONITORS + + elif "All Mobile" == self.device_type: + filter_type = MOBILE + + elif "All DataStorage" == self.device_type: + filter_type = DATASTORAGE + + elif "All Peripherals" == self.device_type: + filter_type = PERIPHERALS + + if filter_type: + self.devices = self.devices.filter(Device.type.in_(filter_type)) + + return self.devices.order_by(Device.updated.desc()) class LotForm(FlaskForm): @@ -208,12 +300,12 @@ class UploadSnapshotForm(FlaskForm, SnapshotMix): except ValidationError as err: txt = "{}".format(err) uuid = snapshot_json.get('uuid') - wbid = snapshot_json.get('wbid') + sid = snapshot_json.get('sid') error = SnapshotErrors( description=txt, snapshot_uuid=uuid, severity=Severity.Error, - wbid=wbid, + sid=sid, ) error.save(commit=True) self.result[filename] = 'Error' @@ -468,7 +560,7 @@ class TagDeviceForm(FlaskForm): db.session.commit() -class NewActionForm(FlaskForm): +class ActionFormMix(FlaskForm): name = StringField( 'Name', [validators.length(max=50)], @@ -500,17 +592,23 @@ class NewActionForm(FlaskForm): if not is_valid: return False - self._devices = OrderedSet() - if self.devices.data: - devices = set(self.devices.data.split(",")) - self._devices = OrderedSet( - Device.query.filter(Device.id.in_(devices)) - .filter(Device.owner_id == g.user.id) - .all() - ) + if self.type.data in [None, '']: + return False - if not self._devices: - return False + if not self.devices.data: + return False + + self._devices = OrderedSet() + + devices = set(self.devices.data.split(",")) + self._devices = OrderedSet( + Device.query.filter(Device.id.in_(devices)) + .filter(Device.owner_id == g.user.id) + .all() + ) + + if not self._devices: + return False return True @@ -543,29 +641,83 @@ class NewActionForm(FlaskForm): return self.type.data -class AllocateForm(NewActionForm): - 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') - +class NewActionForm(ActionFormMix): def validate(self, extra_validators=None): is_valid = super().validate(extra_validators) + if not is_valid: + return False + + if self.type.data in ['Allocate', 'Deallocate', 'Trade', 'DataWipe']: + return False + + return True + + +class AllocateForm(ActionFormMix): + start_time = DateField('Start time') + 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): + 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): @@ -621,7 +773,7 @@ class DataWipeDocumentForm(Form): return self._obj -class DataWipeForm(NewActionForm): +class DataWipeForm(ActionFormMix): document = FormField(DataWipeDocumentForm) def save(self): @@ -648,7 +800,7 @@ class DataWipeForm(NewActionForm): return self.instance -class TradeForm(NewActionForm): +class TradeForm(ActionFormMix): user_from = StringField( 'Supplier', [validators.Optional()], @@ -695,6 +847,9 @@ class TradeForm(NewActionForm): email_from = self.user_from.data email_to = self.user_to.data + if self.type.data != "Trade": + return False + if not self.confirm.data and not self.code.data: self.code.errors = ["If you don't want to confirm, you need a code"] is_valid = False diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 9812ffad..6f3174d5 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -7,10 +7,9 @@ import flask_weasyprint from flask import Blueprint, g, make_response, request, url_for from flask.views import View from flask_login import current_user, login_required -from sqlalchemy import or_ from werkzeug.exceptions import NotFound -from ereuse_devicehub import __version__, messages +from ereuse_devicehub import messages from ereuse_devicehub.db import db from ereuse_devicehub.inventory.forms import ( AllocateForm, @@ -32,35 +31,21 @@ from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.tag.model import Tag +from ereuse_devicehub.views import GenericMixView devices = Blueprint('inventory', __name__, url_prefix='/inventory') logger = logging.getLogger(__name__) -class GenericMixView(View): - def get_lots(self): - return ( - Lot.query.outerjoin(Trade) - .filter( - or_( - Trade.user_from == g.user, - Trade.user_to == g.user, - Lot.owner_id == g.user.id, - ) - ) - .distinct() - ) - - class DeviceListMix(GenericMixView): - decorators = [login_required] template_name = 'inventory/device_list.html' def get_context(self, lot_id): - form_filter = FilterForm() - filter_types = form_filter.search() - lots = self.get_lots() + super().get_context() + lots = self.context['lots'] + form_filter = FilterForm(lots, lot_id) + devices = form_filter.search() lot = None tags = ( Tag.query.filter(Tag.owner_id == current_user.id) @@ -70,10 +55,6 @@ class DeviceListMix(GenericMixView): if lot_id: lot = lots.filter(Lot.id == lot_id).one() - devices = lot.devices - if "All" not in filter_types: - devices = [dev for dev in lot.devices if dev.type in filter_types] - devices = sorted(devices, key=lambda x: x.updated, reverse=True) form_new_action = NewActionForm(lot=lot.id) form_new_allocate = AllocateForm(lot=lot.id) form_new_datawipe = DataWipeForm(lot=lot.id) @@ -83,20 +64,6 @@ class DeviceListMix(GenericMixView): user_from=g.user.email, ) else: - if "All" in filter_types: - devices = ( - Device.query.filter(Device.owner_id == current_user.id) - .filter_by(lots=None) - .order_by(Device.updated.desc()) - ) - else: - devices = ( - Device.query.filter(Device.owner_id == current_user.id) - .filter_by(lots=None) - .filter(Device.type.in_(filter_types)) - .order_by(Device.updated.desc()) - ) - form_new_action = NewActionForm() form_new_allocate = AllocateForm() form_new_datawipe = DataWipeForm() @@ -106,21 +73,21 @@ class DeviceListMix(GenericMixView): if action_devices: list_devices.extend([int(x) for x in action_devices.split(",")]) - self.context = { - 'devices': devices, - 'lots': lots, - 'form_tag_device': TagDeviceForm(), - 'form_new_action': form_new_action, - 'form_new_allocate': form_new_allocate, - 'form_new_datawipe': form_new_datawipe, - 'form_new_trade': form_new_trade, - 'form_filter': form_filter, - 'form_print_labels': PrintLabelsForm(), - 'lot': lot, - 'tags': tags, - 'list_devices': list_devices, - 'version': __version__, - } + 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_trade': form_new_trade, + 'form_filter': form_filter, + 'form_print_labels': PrintLabelsForm(), + 'lot': lot, + 'tags': tags, + 'list_devices': list_devices, + } + ) return self.context @@ -136,20 +103,20 @@ class DeviceDetailView(GenericMixView): template_name = 'inventory/device_detail.html' def dispatch_request(self, id): - lots = self.get_lots() + self.get_context() device = ( Device.query.filter(Device.owner_id == current_user.id) .filter(Device.devicehub_id == id) .one() ) - context = { - 'device': device, - 'lots': lots, - 'page_title': 'Device {}'.format(device.devicehub_id), - 'version': __version__, - } - return flask.render_template(self.template_name, **context) + self.context.update( + { + 'device': device, + 'page_title': 'Device {}'.format(device.devicehub_id), + } + ) + return flask.render_template(self.template_name, **self.context) class LotCreateView(GenericMixView): @@ -165,17 +132,17 @@ class LotCreateView(GenericMixView): next_url = url_for('inventory.lotdevicelist', lot_id=form.id) return flask.redirect(next_url) - lots = self.get_lots() - context = { - 'form': form, - 'title': self.title, - 'lots': lots, - 'version': __version__, - } - return flask.render_template(self.template_name, **context) + self.get_context() + self.context.update( + { + 'form': form, + 'title': self.title, + } + ) + return flask.render_template(self.template_name, **self.context) -class LotUpdateView(View): +class LotUpdateView(GenericMixView): methods = ['GET', 'POST'] decorators = [login_required] template_name = 'inventory/lot.html' @@ -188,14 +155,14 @@ class LotUpdateView(View): next_url = url_for('inventory.lotdevicelist', lot_id=id) return flask.redirect(next_url) - lots = Lot.query.filter(Lot.owner_id == current_user.id) - context = { - 'form': form, - 'title': self.title, - 'lots': lots, - 'version': __version__, - } - return flask.render_template(self.template_name, **context) + self.get_context() + self.context.update( + { + 'form': form, + 'title': self.title, + } + ) + return flask.render_template(self.template_name, **self.context) class LotDeleteView(View): @@ -222,25 +189,26 @@ class UploadSnapshotView(GenericMixView): template_name = 'inventory/upload_snapshot.html' def dispatch_request(self, lot_id=None): - lots = self.get_lots() + self.get_context() form = UploadSnapshotForm() - context = { - 'page_title': 'Upload Snapshot', - 'lots': lots, - 'form': form, - 'lot_id': lot_id, - 'version': __version__, - } + self.context.update( + { + 'page_title': 'Upload Snapshot', + 'form': form, + 'lot_id': lot_id, + } + ) if form.validate_on_submit(): snapshot, devices = form.save(commit=False) if lot_id: + lots = self.context['lots'] lot = lots.filter(Lot.id == lot_id).one() for dev in devices: lot.devices.add(dev) db.session.add(lot) db.session.commit() - return flask.render_template(self.template_name, **context) + return flask.render_template(self.template_name, **self.context) class DeviceCreateView(GenericMixView): @@ -249,20 +217,21 @@ class DeviceCreateView(GenericMixView): template_name = 'inventory/device_create.html' def dispatch_request(self, lot_id=None): - lots = self.get_lots() + self.get_context() form = NewDeviceForm() - context = { - 'page_title': 'New Device', - 'lots': lots, - 'form': form, - 'lot_id': lot_id, - 'version': __version__, - } + self.context.update( + { + 'page_title': 'New Device', + 'form': form, + 'lot_id': lot_id, + } + ) if form.validate_on_submit(): snapshot = form.save(commit=False) next_url = url_for('inventory.devicelist') if lot_id: next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) + lots = self.context['lots'] lot = lots.filter(Lot.id == lot_id).one() lot.devices.add(snapshot.device) db.session.add(lot) @@ -271,7 +240,7 @@ class DeviceCreateView(GenericMixView): messages.success('Device "{}" created successfully!'.format(form.type.data)) return flask.redirect(next_url) - return flask.render_template(self.template_name, **context) + return flask.render_template(self.template_name, **self.context) class TagLinkDeviceView(View): @@ -287,13 +256,13 @@ class TagLinkDeviceView(View): return flask.redirect(request.referrer) -class TagUnlinkDeviceView(View): +class TagUnlinkDeviceView(GenericMixView): methods = ['POST', 'GET'] decorators = [login_required] template_name = 'inventory/tag_unlink_device.html' def dispatch_request(self, id): - lots = Lot.query.filter(Lot.owner_id == current_user.id) + self.get_context() form = TagDeviceForm(delete=True, device=id) if form.validate_on_submit(): form.remove() @@ -301,14 +270,15 @@ class TagUnlinkDeviceView(View): next_url = url_for('inventory.devicelist') return flask.redirect(next_url) - return flask.render_template( - self.template_name, - form=form, - lots=lots, - referrer=request.referrer, - version=__version__, + self.context.update( + { + 'form': form, + 'referrer': request.referrer, + } ) + return flask.render_template(self.template_name, **self.context) + class NewActionView(View): methods = ['POST'] @@ -317,16 +287,19 @@ class NewActionView(View): def dispatch_request(self): self.form = self.form_class() + next_url = self.get_next_url() if self.form.validate_on_submit(): self.form.save() messages.success( 'Action "{}" created successfully!'.format(self.form.type.data) ) - next_url = self.get_next_url() return flask.redirect(next_url) + messages.error('Action {} error!'.format(self.form.type.data)) + return flask.redirect(next_url) + def get_next_url(self): lot_id = self.form.lot.data @@ -352,10 +325,12 @@ class NewAllocateView(NewActionView, DeviceListMix): next_url = self.get_next_url() return flask.redirect(next_url) - lot_id = self.form.lot.data - self.get_context(lot_id) - self.context['form_new_allocate'] = self.form - return flask.render_template(self.template_name, **self.context) + messages.error('Action {} error!'.format(self.form.type.data)) + for k, v in self.form.errors.items(): + value = ';'.join(v) + messages.error('Action Error {key}: {value}!'.format(key=k, value=value)) + next_url = self.get_next_url() + return flask.redirect(next_url) class NewDataWipeView(NewActionView, DeviceListMix): @@ -374,10 +349,9 @@ class NewDataWipeView(NewActionView, DeviceListMix): next_url = self.get_next_url() return flask.redirect(next_url) - lot_id = self.form.lot.data - self.get_context(lot_id) - self.context['form_new_datawipe'] = self.form - return flask.render_template(self.template_name, **self.context) + messages.error('Action {} error!'.format(self.form.type.data)) + next_url = self.get_next_url() + return flask.redirect(next_url) class NewTradeView(NewActionView, DeviceListMix): @@ -396,10 +370,9 @@ class NewTradeView(NewActionView, DeviceListMix): next_url = self.get_next_url() return flask.redirect(next_url) - lot_id = self.form.lot.data - self.get_context(lot_id) - self.context['form_new_trade'] = self.form - return flask.render_template(self.template_name, **self.context) + messages.error('Action {} error!'.format(self.form.type.data)) + next_url = self.get_next_url() + return flask.redirect(next_url) class NewTradeDocumentView(View): @@ -411,6 +384,7 @@ class NewTradeDocumentView(View): def dispatch_request(self, lot_id): self.form = self.form_class(lot=lot_id) + self.get_context() if self.form.validate_on_submit(): self.form.save() @@ -418,9 +392,8 @@ class NewTradeDocumentView(View): next_url = url_for('inventory.lotdevicelist', lot_id=lot_id) return flask.redirect(next_url) - return flask.render_template( - self.template_name, form=self.form, title=self.title, version=__version__ - ) + self.context.update({'form': self.form, 'title': self.title}) + return flask.render_template(self.template_name, **self.context) class ExportsView(View): @@ -432,7 +405,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: @@ -508,19 +480,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/labels/forms.py b/ereuse_devicehub/labels/forms.py index cd4b5bec..98427215 100644 --- a/ereuse_devicehub/labels/forms.py +++ b/ereuse_devicehub/labels/forms.py @@ -64,10 +64,7 @@ class PrintLabelsForm(FlaskForm): .all() ) - # print only tags that are DHID - dhids = [x.devicehub_id for x in self._devices] - self._tags = ( - Tag.query.filter(Tag.owner_id == g.user.id).filter(Tag.id.in_(dhids)).all() - ) + if not self._devices: + return False return is_valid diff --git a/ereuse_devicehub/labels/views.py b/ereuse_devicehub/labels/views.py index 445a4eb8..e7fc3b0d 100644 --- a/ereuse_devicehub/labels/views.py +++ b/ereuse_devicehub/labels/views.py @@ -27,7 +27,7 @@ class TagListView(View): context = { 'lots': lots, 'tags': tags, - 'page_title': 'Tags Management', + 'page_title': 'Unique Identifiers Management', 'version': __version__, } return flask.render_template(self.template_name, **context) @@ -102,7 +102,7 @@ class PrintLabelsView(View): form = PrintLabelsForm() if form.validate_on_submit(): context['form'] = form - context['tags'] = form._tags + context['devices'] = form._devices return flask.render_template(self.template_name, **context) else: messages.error('Error you need select one or more devices') diff --git a/ereuse_devicehub/migrations/versions/6f6771813f2e_change_wbid_for_sid.py b/ereuse_devicehub/migrations/versions/6f6771813f2e_change_wbid_for_sid.py new file mode 100644 index 00000000..7eac98fd --- /dev/null +++ b/ereuse_devicehub/migrations/versions/6f6771813f2e_change_wbid_for_sid.py @@ -0,0 +1,66 @@ +"""change wbid for sid + +Revision ID: 6f6771813f2e +Revises: 97bef94f7982 +Create Date: 2022-04-25 10:52:11.767569 + +""" +import citext +import sqlalchemy as sa +from alembic import context, op + +# revision identifiers, used by Alembic. +revision = '6f6771813f2e' +down_revision = '97bef94f7982' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + + +def upgrade_datas(): + con = op.get_bind() + sql = f"select * from {get_inv()}.snapshot;" + snapshots = con.execute(sql) + for snap in snapshots: + wbid = snap.wbid + if wbid: + sql = f"""update {get_inv()}.snapshot set sid='{wbid}' + where wbid='{wbid}';""" + con.execute(sql) + + sql = f"select wbid from {get_inv()}.snapshot_errors;" + snapshots = con.execute(sql) + for snap in snapshots: + wbid = snap.wbid + if wbid: + sql = f"""update {get_inv()}.snapshot set sid='{wbid}' + where wbid='{wbid}';""" + con.execute(sql) + + +def upgrade(): + op.add_column( + 'snapshot', + sa.Column('sid', citext.CIText(), nullable=True), + schema=f'{get_inv()}', + ) + + op.add_column( + 'snapshot_errors', + sa.Column('sid', citext.CIText(), nullable=True), + schema=f'{get_inv()}', + ) + upgrade_datas() + op.drop_column('snapshot', 'wbid', schema=f'{get_inv()}') + op.drop_column('snapshot_errors', 'wbid', schema=f'{get_inv()}') + + +def downgrade(): + op.drop_column('snapshot', 'sid', schema=f'{get_inv()}') + op.drop_column('snapshot_errors', 'sid', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/parser/computer.py b/ereuse_devicehub/parser/computer.py index cdfe7a8b..bf417dac 100644 --- a/ereuse_devicehub/parser/computer.py +++ b/ereuse_devicehub/parser/computer.py @@ -1,3 +1,4 @@ +import logging import re from contextlib import suppress from datetime import datetime @@ -13,7 +14,11 @@ from ereuse_utils.nested_lookup import ( ) from ereuse_devicehub.parser import base2, unit, utils +from ereuse_devicehub.parser.models import SnapshotErrors from ereuse_devicehub.parser.utils import Dumpeable +from ereuse_devicehub.resources.enums import Severity + +logger = logging.getLogger(__name__) class Device(Dumpeable): @@ -417,7 +422,7 @@ class Computer(Device): self._ram = None @classmethod - def run(cls, lshw, hwinfo_raw): + def run(cls, lshw, hwinfo_raw, uuid=None, sid=None): """ Gets hardware information from the computer and its components, like serial numbers or model names, and benchmarks them. @@ -428,17 +433,35 @@ class Computer(Device): hwinfo = hwinfo_raw.splitlines() computer = cls(lshw) components = [] - for Component in cls.COMPONENTS: - if Component == Display and computer.type != 'Laptop': - continue # Only get display info when computer is laptop - components.extend(Component.new(lshw=lshw, hwinfo=hwinfo)) - components.append(Motherboard.new(lshw, hwinfo)) + try: + for Component in cls.COMPONENTS: + if Component == Display and computer.type != 'Laptop': + continue # Only get display info when computer is laptop + components.extend(Component.new(lshw=lshw, hwinfo=hwinfo)) + components.append(Motherboard.new(lshw, hwinfo)) + computer._ram = sum( + ram.size for ram in components if isinstance(ram, RamModule) + ) + except Exception as err: + # if there are any problem with components, save the problem and continue + txt = "Error: Snapshot: {uuid}, sid: {sid}, type_error: {type}, error: {error}".format( + uuid=uuid, sid=sid, type=err.__class__, error=err + ) + cls.errors(txt, uuid=uuid, sid=sid) - computer._ram = sum( - ram.size for ram in components if isinstance(ram, RamModule) - ) return computer, components + @classmethod + def errors(cls, txt=None, uuid=None, sid=None, severity=Severity.Error): + if not txt: + return + + logger.error(txt) + error = SnapshotErrors( + description=txt, snapshot_uuid=uuid, severity=severity, sid=sid + ) + error.save() + def __str__(self) -> str: specs = super().__str__() return '{} with {} MB of RAM.'.format(specs, self._ram) diff --git a/ereuse_devicehub/parser/models.py b/ereuse_devicehub/parser/models.py index edec89af..54f02461 100644 --- a/ereuse_devicehub/parser/models.py +++ b/ereuse_devicehub/parser/models.py @@ -14,7 +14,7 @@ class SnapshotErrors(Thing): id = Column(BigInteger, Sequence('snapshot_errors_seq'), primary_key=True) description = Column(CIText(), default='', nullable=False) - wbid = Column(CIText(), nullable=True) + sid = Column(CIText(), nullable=True) severity = Column(SmallInteger, default=Severity.Info, nullable=False) snapshot_uuid = Column(UUID(as_uuid=True), nullable=False) owner_id = db.Column( diff --git a/ereuse_devicehub/parser/parser.py b/ereuse_devicehub/parser/parser.py index fc46d351..85683a5c 100644 --- a/ereuse_devicehub/parser/parser.py +++ b/ereuse_devicehub/parser/parser.py @@ -3,7 +3,8 @@ import logging import uuid from dmidecode import DMIParse -from marshmallow import ValidationError +from flask import request +from marshmallow.exceptions import ValidationError from ereuse_devicehub.parser import base2 from ereuse_devicehub.parser.computer import Computer @@ -38,7 +39,7 @@ class ParseSnapshot: "version": "14.0.0", "endTime": snapshot["timestamp"], "elapsed": 1, - "wbid": snapshot["wbid"], + "sid": snapshot["sid"], } def get_snapshot(self): @@ -120,7 +121,11 @@ class ParseSnapshot: def get_usb_num(self): return len( - [u for u in self.dmi.get("Port Connector") if u.get("Port Type") == "USB"] + [ + u + for u in self.dmi.get("Port Connector") + if "USB" in u.get("Port Type", "").upper() + ] ) def get_serial_num(self): @@ -128,7 +133,7 @@ class ParseSnapshot: [ u for u in self.dmi.get("Port Connector") - if u.get("Port Type") == "SERIAL" + if "SERIAL" in u.get("Port Type", "").upper() ] ) @@ -137,7 +142,7 @@ class ParseSnapshot: [ u for u in self.dmi.get("Port Connector") - if u.get("Port Type") == "PCMCIA" + if "PCMCIA" in u.get("Port Type", "").upper() ] ) @@ -314,7 +319,7 @@ class ParseSnapshotLsHw: def __init__(self, snapshot, default="n/a"): self.default = default self.uuid = snapshot.get("uuid") - self.wbid = snapshot.get("wbid") + self.sid = snapshot.get("sid") self.dmidecode_raw = snapshot["data"]["dmidecode"] self.smart = snapshot["data"]["smart"] self.hwinfo_raw = snapshot["data"]["hwinfo"] @@ -339,21 +344,11 @@ class ParseSnapshotLsHw: "version": "14.0.0", "endTime": snapshot["timestamp"], "elapsed": 1, - "wbid": snapshot["wbid"], + "sid": snapshot["sid"], } def get_snapshot(self): - try: - return Snapshot().load(self.snapshot_json) - except ValidationError as err: - txt = "{}".format(err) - uuid = self.snapshot_json.get('uuid') - wbid = self.snapshot_json.get('wbid') - error = SnapshotErrors( - description=txt, snapshot_uuid=uuid, severity=Severity.Error, wbid=wbid - ) - error.save(commit=True) - raise err + return Snapshot().load(self.snapshot_json) def parse_hwinfo(self): hw_blocks = self.hwinfo_raw.split("\n\n") @@ -365,8 +360,24 @@ class ParseSnapshotLsHw: return x def set_basic_datas(self): - pc, self.components_obj = Computer.run(self.lshw, self.hwinfo_raw) - self.device = pc.dump() + try: + pc, self.components_obj = Computer.run( + self.lshw, self.hwinfo_raw, self.uuid, self.sid + ) + pc = pc.dump() + minimum_hid = None in [pc['manufacturer'], pc['model'], pc['serialNumber']] + if minimum_hid and not self.components_obj: + # if no there are hid and any components return 422 + raise Exception + except Exception: + msg = """It has not been possible to create the device because we lack data. + You can find more information at: {}""".format( + request.url_root + ) + txt = json.dumps({'sid': self.sid, 'message': msg}) + raise ValidationError(txt) + + self.device = pc self.device['uuid'] = self.get_uuid() def set_components(self): @@ -405,8 +416,10 @@ class ParseSnapshotLsHw: def get_ram_size(self, ram): size = ram.get("Size") if not len(size.split(" ")) == 2: - txt = "Error: Snapshot: {uuid}, tag: {wbid} have this ram Size: {size}".format( - uuid=self.uuid, size=size, wbid=self.wbid + txt = ( + "Error: Snapshot: {uuid}, tag: {sid} have this ram Size: {size}".format( + uuid=self.uuid, size=size, sid=self.sid + ) ) self.errors(txt) return 128 @@ -416,8 +429,8 @@ class ParseSnapshotLsHw: def get_ram_speed(self, ram): speed = ram.get("Speed", "100") if not len(speed.split(" ")) == 2: - txt = "Error: Snapshot: {uuid}, tag: {wbid} have this ram Speed: {speed}".format( - uuid=self.uuid, speed=speed, wbid=self.wbid + txt = "Error: Snapshot: {uuid}, tag: {sid} have this ram Speed: {speed}".format( + uuid=self.uuid, speed=speed, sid=self.sid ) self.errors(txt) return 100 @@ -451,8 +464,8 @@ class ParseSnapshotLsHw: uuid.UUID(dmi_uuid) except (ValueError, AttributeError) as err: self.errors("{}".format(err)) - txt = "Error: Snapshot: {uuid} tag: {wbid} have this uuid: {device}".format( - uuid=self.uuid, device=dmi_uuid, wbid=self.wbid + txt = "Error: Snapshot: {uuid} tag: {sid} have this uuid: {device}".format( + uuid=self.uuid, device=dmi_uuid, sid=self.sid ) self.errors(txt) dmi_uuid = None @@ -461,6 +474,8 @@ class ParseSnapshotLsHw: def get_data_storage(self): for sm in self.smart: + if sm.get('smartctl', {}).get('exit_status') == 1: + continue model = sm.get('model_name') manufacturer = None if model and len(model.split(" ")) > 1: @@ -487,7 +502,7 @@ class ParseSnapshotLsHw: SSD = 'SolidStateDrive' HDD = 'HardDrive' type_dev = x.get('device', {}).get('type') - trim = x.get("trim", {}).get("supported") == "true" + trim = x.get('trim', {}).get("supported") in [True, "true"] return SSD if type_dev in SSDS or trim else HDD def get_data_storage_interface(self, x): @@ -496,22 +511,21 @@ class ParseSnapshotLsHw: DataStorageInterface(interface.upper()) except ValueError as err: txt = "tag: {}, interface {} is not in DataStorageInterface Enum".format( - interface, self.wbid + interface, self.sid ) self.errors("{}".format(err)) self.errors(txt) return "ATA" def get_data_storage_size(self, x): - type_dev = x.get('device', {}).get('protocol', '').lower() - total_capacity = "{type}_total_capacity".format(type=type_dev) - if not x.get(total_capacity): + total_capacity = x.get('user_capacity', {}).get('bytes') + if not total_capacity: return 1 # convert bytes to Mb - return x.get(total_capacity) / 1024**2 + return total_capacity / 1024**2 def get_test_data_storage(self, smart): - log = "smart_health_information_log" + hours = smart.get("power_on_time", {}).get('hours', 0) action = { "status": "Completed without error", "reallocatedSectorCount": smart.get("reallocated_sector_count", 0), @@ -519,7 +533,8 @@ class ParseSnapshotLsHw: "assessment": True, "severity": "Info", "offlineUncorrectable": smart.get("offline_uncorrectable", 0), - "lifetime": 0, + "lifetime": hours, + "powerOnHours": hours, "type": "TestDataStorage", "length": "Short", "elapsed": 0, @@ -529,11 +544,6 @@ class ParseSnapshotLsHw: "powerCycleCount": smart.get("power_cycle_count", 0), } - for k in smart.keys(): - if log in k: - action['lifetime'] = smart[k].get("power_on_hours", 0) - action['powerOnHours'] = smart[k].get("power_on_hours", 0) - return action def errors(self, txt=None, severity=Severity.Info): @@ -543,6 +553,6 @@ class ParseSnapshotLsHw: logger.error(txt) self._errors.append(txt) error = SnapshotErrors( - description=txt, snapshot_uuid=self.uuid, severity=severity, wbid=self.wbid + description=txt, snapshot_uuid=self.uuid, severity=severity, sid=self.sid ) error.save() diff --git a/ereuse_devicehub/parser/schemas.py b/ereuse_devicehub/parser/schemas.py index f65f9090..50fc698f 100644 --- a/ereuse_devicehub/parser/schemas.py +++ b/ereuse_devicehub/parser/schemas.py @@ -7,11 +7,11 @@ from ereuse_devicehub.resources.schemas import Thing class Snapshot_lite_data(MarshmallowSchema): - dmidecode = String(required=False) - hwinfo = String(required=False) - smart = List(Dict(), required=False) - lshw = Dict(required=False) - lspci = String(required=False) + dmidecode = String(required=True) + hwinfo = String(required=True) + smart = List(Dict(), required=True) + lshw = Dict(required=True) + lspci = String(required=True) class Snapshot_lite(Thing): @@ -19,10 +19,10 @@ class Snapshot_lite(Thing): version = String(required=True) schema_api = String(required=True) software = String(required=True) - wbid = String(required=True) + sid = String(required=True) type = String(required=True) timestamp = String(required=True) - data = Nested(Snapshot_lite_data) + data = Nested(Snapshot_lite_data, required=True) @validates_schema def validate_workbench_version(self, data: dict): diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index 92805ab5..c0876598 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -17,12 +17,12 @@ from datetime import datetime, timedelta, timezone from decimal import ROUND_HALF_EVEN, ROUND_UP, Decimal from typing import Optional, Set, Union from uuid import uuid4 -from dateutil.tz import tzutc import inflection import teal.db from boltons import urlutils from citext import CIText +from dateutil.tz import tzutc from flask import current_app as app from flask import g from sortedcontainers import SortedSet @@ -274,7 +274,9 @@ class Action(Thing): super().__init__(**kwargs) def __lt__(self, other): - return self.end_time.replace(tzinfo=tzutc()) < other.end_time.replace(tzinfo=tzutc()) + return self.end_time.replace(tzinfo=tzutc()) < other.end_time.replace( + tzinfo=tzutc() + ) def __str__(self) -> str: return '{}'.format(self.severity) @@ -664,7 +666,7 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice): elapsed.comment = """For Snapshots made with Workbench, the total amount of time it took to complete. """ - wbid = Column(CIText(), nullable=True) + sid = Column(CIText(), nullable=True) def get_last_lifetimes(self): """We get the lifetime and serial_number of the first disk""" diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index 5c8f2d5f..9dadab2c 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -425,7 +425,7 @@ class Snapshot(ActionWithOneDevice): See docs for more info. """ uuid = UUID() - wbid = String(required=False) + sid = String(required=False) software = EnumField( SnapshotSoftware, required=True, diff --git a/ereuse_devicehub/resources/agent/models.py b/ereuse_devicehub/resources/agent/models.py index ab9e073a..826d0545 100644 --- a/ereuse_devicehub/resources/agent/models.py +++ b/ereuse_devicehub/resources/agent/models.py @@ -3,7 +3,9 @@ from operator import attrgetter from uuid import uuid4 from citext import CIText -from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint +from sqlalchemy import Column +from sqlalchemy import Enum as DBEnum +from sqlalchemy import ForeignKey, Unicode, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import backref, relationship, validates @@ -31,7 +33,7 @@ class Agent(Thing): name = Column(CIText()) name.comment = """The name of the organization or person.""" tax_id = Column(Unicode(length=STR_SM_SIZE), check_lower('tax_id')) - tax_id.comment = """The Tax / Fiscal ID of the organization, + tax_id.comment = """The Tax / Fiscal ID of the organization, e.g. the TIN in the US or the CIF/NIF in Spain. """ country = Column(DBEnum(enums.Country)) @@ -42,7 +44,7 @@ class Agent(Thing): __table_args__ = ( UniqueConstraint(tax_id, country, name='Registration Number per country.'), UniqueConstraint(tax_id, name, name='One tax ID with one name.'), - db.Index('agent_type', type, postgresql_using='hash') + db.Index('agent_type', type, postgresql_using='hash'), ) @declared_attr @@ -63,7 +65,9 @@ class Agent(Thing): @property def actions(self) -> list: # todo test - return sorted(chain(self.actions_agent, self.actions_to), key=attrgetter('created')) + return sorted( + chain(self.actions_agent, self.actions_to), key=attrgetter('created') + ) @validates('name') def does_not_contain_slash(self, _, value: str): @@ -76,15 +80,17 @@ class Agent(Thing): class Organization(JoinedTableMixin, Agent): - default_of = db.relationship(Inventory, - uselist=False, - lazy=True, - backref=backref('org', lazy=True), - # We need to use this as we cannot do Inventory.foreign -> Org - # as foreign keys can only reference to one table - # and we have multiple organization table (one per schema) - foreign_keys=[Inventory.org_id], - primaryjoin=lambda: Organization.id == Inventory.org_id) + default_of = db.relationship( + Inventory, + uselist=False, + lazy=True, + backref=backref('org', lazy=True), + # We need to use this as we cannot do Inventory.foreign -> Org + # as foreign keys can only reference to one table + # and we have multiple organization table (one per schema) + foreign_keys=[Inventory.org_id], + primaryjoin=lambda: Organization.id == Inventory.org_id, + ) def __init__(self, name: str, **kwargs) -> None: super().__init__(**kwargs, name=name) @@ -97,12 +103,17 @@ class Organization(JoinedTableMixin, Agent): class Individual(JoinedTableMixin, Agent): active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id)) - active_org = relationship(Organization, primaryjoin=active_org_id == Organization.id) + + active_org = relationship( + Organization, primaryjoin=active_org_id == Organization.id + ) user_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True) - user = relationship(User, - backref=backref('individuals', lazy=True, collection_class=set), - primaryjoin=user_id == User.id) + user = relationship( + User, + backref=backref('individuals', lazy=True, collection_class=set), + primaryjoin=user_id == User.id, + ) class Membership(Thing): @@ -110,20 +121,29 @@ class Membership(Thing): For example, because the individual works in or because is a member of. """ - id = Column(Unicode(), check_lower('id')) - organization_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True) - organization = relationship(Organization, - backref=backref('members', collection_class=set, lazy=True), - primaryjoin=organization_id == Organization.id) - individual_id = Column(UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True) - individual = relationship(Individual, - backref=backref('member_of', collection_class=set, lazy=True), - primaryjoin=individual_id == Individual.id) - def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None: - super().__init__(organization=organization, - individual=individual, - id=id) + id = Column(Unicode(), check_lower('id')) + organization_id = Column( + UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True + ) + organization = relationship( + Organization, + backref=backref('members', collection_class=set, lazy=True), + primaryjoin=organization_id == Organization.id, + ) + individual_id = Column( + UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True + ) + individual = relationship( + Individual, + backref=backref('member_of', collection_class=set, lazy=True), + primaryjoin=individual_id == Individual.id, + ) + + def __init__( + self, organization: Organization, individual: Individual, id: str = None + ) -> None: + super().__init__(organization=organization, individual=individual, id=id) __table_args__ = ( UniqueConstraint(id, organization_id, name='One member id per organization.'), @@ -134,6 +154,7 @@ class Person(Individual): """A person in the system. There can be several persons pointing to a real. """ + pass diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 9ba23029..1bd33cf6 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -1,20 +1,30 @@ -import pathlib import copy +import pathlib import time -from flask import g from contextlib import suppress from fractions import Fraction from itertools import chain from operator import attrgetter from typing import Dict, List, Set -from flask_sqlalchemy import event from boltons import urlutils from citext import CIText from ereuse_utils.naming import HID_CONVERSION_DOC, Naming +from flask import g, request +from flask_sqlalchemy import event from more_itertools import unique_everseen -from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \ - Sequence, SmallInteger, Unicode, inspect, text +from sqlalchemy import BigInteger, Boolean, Column +from sqlalchemy import Enum as DBEnum +from sqlalchemy import ( + Float, + ForeignKey, + Integer, + Sequence, + SmallInteger, + Unicode, + inspect, + text, +) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.hybrid import hybrid_property @@ -22,19 +32,41 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship, validates from sqlalchemy.util import OrderedSet from sqlalchemy_utils import ColorType from stdnum import imei, meid -from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, \ - check_lower, check_range, IntEnum +from teal.db import ( + CASCADE_DEL, + POLYMORPHIC_ID, + POLYMORPHIC_ON, + URL, + IntEnum, + ResourceNotFound, + check_lower, + check_range, +) from teal.enums import Layouts from teal.marshmallow import ValidationError from teal.resource import url_for_resource from ereuse_devicehub.db import db -from ereuse_devicehub.resources.utils import hashcode -from ereuse_devicehub.resources.enums import BatteryTechnology, CameraFacing, ComputerChassis, \ - DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity, TransferState -from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing, listener_reset_field_updated_in_actual_time -from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.device.metrics import Metrics +from ereuse_devicehub.resources.enums import ( + BatteryTechnology, + CameraFacing, + ComputerChassis, + DataStorageInterface, + DisplayTech, + PrinterTechnology, + RamFormat, + RamInterface, + Severity, + TransferState, +) +from ereuse_devicehub.resources.models import ( + STR_SM_SIZE, + Thing, + listener_reset_field_updated_in_actual_time, +) +from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.resources.utils import hashcode def create_code(context): @@ -58,17 +90,21 @@ class Device(Thing): Devices can contain ``Components``, which are just a type of device (it is a recursive relationship). """ + id = Column(BigInteger, Sequence('device_seq'), primary_key=True) id.comment = """The identifier of the device for this database. Used only internally for software; users should not use this. """ type = Column(Unicode(STR_SM_SIZE), nullable=False) hid = Column(Unicode(), check_lower('hid'), unique=False) - hid.comment = """The Hardware ID (HID) is the ID traceability + hid.comment = ( + """The Hardware ID (HID) is the ID traceability systems use to ID a device globally. This field is auto-generated from Devicehub using literal identifiers from the device, so it can re-generated *offline*. - """ + HID_CONVERSION_DOC + """ + + HID_CONVERSION_DOC + ) model = Column(Unicode(), check_lower('model')) model.comment = """The model of the device in lower case. @@ -118,14 +154,18 @@ class Device(Thing): image = db.Column(db.URL) image.comment = "An image of the device." - owner_id = db.Column(UUID(as_uuid=True), - db.ForeignKey(User.id), - nullable=False, - default=lambda: g.user.id) + owner_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id, + ) owner = db.relationship(User, primaryjoin=owner_id == User.id) allocated = db.Column(Boolean, default=False) allocated.comment = "device is allocated or not." - devicehub_id = db.Column(db.CIText(), nullable=True, unique=True, default=create_code) + devicehub_id = db.Column( + db.CIText(), nullable=True, unique=True, default=create_code + ) devicehub_id.comment = "device have a unique code." active = db.Column(Boolean, default=True) @@ -152,12 +192,12 @@ class Device(Thing): 'image', 'allocated', 'devicehub_id', - 'active' + 'active', } __table_args__ = ( db.Index('device_id', id, postgresql_using='hash'), - db.Index('type_index', type, postgresql_using='hash') + db.Index('type_index', type, postgresql_using='hash'), ) def __init__(self, **kw) -> None: @@ -187,7 +227,9 @@ class Device(Thing): for ac in actions_one: ac.real_created = ac.created - return sorted(chain(actions_multiple, actions_one), key=lambda x: x.real_created) + return sorted( + chain(actions_multiple, actions_one), key=lambda x: x.real_created + ) @property def problems(self): @@ -196,8 +238,9 @@ class Device(Thing): There can be up to 3 actions: current Snapshot, current Physical action, current Trading action. """ - from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.action.models import Snapshot + from ereuse_devicehub.resources.device import states + actions = set() with suppress(LookupError, ValueError): actions.add(self.last_action_of(Snapshot)) @@ -217,11 +260,13 @@ class Device(Thing): """ # todo ensure to remove materialized values when start using them # todo or self.__table__.columns if inspect fails - return {c.key: getattr(self, c.key, None) - for c in inspect(self.__class__).attrs - if isinstance(c, ColumnProperty) - and not getattr(c, 'foreign_keys', None) - and c.key not in self._NON_PHYSICAL_PROPS} + return { + c.key: getattr(self, c.key, None) + for c in inspect(self.__class__).attrs + if isinstance(c, ColumnProperty) + and not getattr(c, 'foreign_keys', None) + and c.key not in self._NON_PHYSICAL_PROPS + } @property def public_properties(self) -> Dict[str, object or None]: @@ -234,11 +279,13 @@ class Device(Thing): """ non_public = ['amount', 'transfer_state', 'receiver_id'] hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public - return {c.key: getattr(self, c.key, None) - for c in inspect(self.__class__).attrs - if isinstance(c, ColumnProperty) - and not getattr(c, 'foreign_keys', None) - and c.key not in hide_properties} + return { + c.key: getattr(self, c.key, None) + for c in inspect(self.__class__).attrs + if isinstance(c, ColumnProperty) + and not getattr(c, 'foreign_keys', None) + and c.key not in hide_properties + } @property def public_actions(self) -> List[object]: @@ -250,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.""" @@ -260,6 +312,7 @@ class Device(Thing): """The last Rate of the device.""" with suppress(LookupError, ValueError): from ereuse_devicehub.resources.action.models import Rate + return self.last_action_of(Rate) @property @@ -268,12 +321,14 @@ class Device(Thing): ever been set.""" with suppress(LookupError, ValueError): from ereuse_devicehub.resources.action.models import Price + return self.last_action_of(Price) @property def last_action_trading(self): """which is the last action trading""" from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): return self.last_action_of(*states.Trading.actions()) @@ -287,6 +342,7 @@ class Device(Thing): - Management """ from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): return self.last_action_of(*states.Status.actions()) @@ -300,6 +356,7 @@ class Device(Thing): - Management """ from ereuse_devicehub.resources.device import states + status_actions = [ac.t for ac in states.Status.actions()] history = [] for ac in self.actions: @@ -329,13 +386,15 @@ class Device(Thing): if not hasattr(lot, 'trade'): return - Status = {0: 'Trade', - 1: 'Confirm', - 2: 'NeedConfirmation', - 3: 'TradeConfirmed', - 4: 'Revoke', - 5: 'NeedConfirmRevoke', - 6: 'RevokeConfirmed'} + Status = { + 0: 'Trade', + 1: 'Confirm', + 2: 'NeedConfirmation', + 3: 'TradeConfirmed', + 4: 'Revoke', + 5: 'NeedConfirmRevoke', + 6: 'RevokeConfirmed', + } trade = lot.trade user_from = trade.user_from @@ -408,6 +467,7 @@ class Device(Thing): """If the actual trading state is an revoke action, this property show the id of that revoke""" from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): action = self.last_action_of(*states.Trading.actions()) if action.type == 'Revoke': @@ -417,6 +477,7 @@ class Device(Thing): def physical(self): """The actual physical state, None otherwise.""" from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): action = self.last_action_of(*states.Physical.actions()) return states.Physical(action.__class__) @@ -425,6 +486,7 @@ class Device(Thing): def traking(self): """The actual traking state, None otherwise.""" from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): action = self.last_action_of(*states.Traking.actions()) return states.Traking(action.__class__) @@ -433,6 +495,7 @@ class Device(Thing): def usage(self): """The actual usage state, None otherwise.""" from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): action = self.last_action_of(*states.Usage.actions()) return states.Usage(action.__class__) @@ -470,8 +533,11 @@ class Device(Thing): test has been executed. """ from ereuse_devicehub.resources.action.models import Test - current_tests = unique_everseen((e for e in reversed(self.actions) if isinstance(e, Test)), - key=attrgetter('type')) # last test of each type + + current_tests = unique_everseen( + (e for e in reversed(self.actions) if isinstance(e, Test)), + key=attrgetter('type'), + ) # last test of each type return self._warning_actions(current_tests) @property @@ -496,7 +562,9 @@ class Device(Thing): def set_hid(self): with suppress(TypeError): - self.hid = Naming.hid(self.type, self.manufacturer, self.model, self.serial_number) + self.hid = Naming.hid( + self.type, self.manufacturer, self.model, self.serial_number + ) def last_action_of(self, *types): """Gets the last action of the given types. @@ -509,7 +577,9 @@ class Device(Thing): actions.sort(key=lambda x: x.created) return next(e for e in reversed(actions) if isinstance(e, types)) except StopIteration: - raise LookupError('{!r} does not contain actions of types {}.'.format(self, types)) + raise LookupError( + '{!r} does not contain actions of types {}.'.format(self, types) + ) def which_user_put_this_device_in_trace(self): """which is the user than put this device in this trade""" @@ -546,6 +616,32 @@ class Device(Thing): metrics = Metrics(device=self) return metrics.get_metrics() + def get_type_logo(self): + # This is used for see one logo of type of device in the frontend + types = { + "Desktop": "bi bi-file-post-fill", + "Laptop": "bi bi-laptop", + "Server": "bi bi-server", + "Processor": "bi bi-cpu", + "RamModule": "bi bi-list", + "Motherboard": "bi bi-cpu-fill", + "NetworkAdapter": "bi bi-hdd-network", + "GraphicCard": "bi bi-brush", + "SoundCard": "bi bi-volume-up-fill", + "Monitor": "bi bi-display", + "Display": "bi bi-display", + "ComputerMonitor": "bi bi-display", + "TelevisionSet": "bi bi-easel", + "TV": "bi bi-easel", + "Projector": "bi bi-camera-video", + "Tablet": "bi bi-tablet-landscape", + "Smartphone": "bi bi-phone", + "Cellphone": "bi bi-telephone", + "HardDrive": "bi bi-hdd-stack", + "SolidStateDrive": "bi bi-hdd", + } + return types.get(self.type, '') + def __lt__(self, other): return self.id < other.id @@ -571,19 +667,24 @@ class Device(Thing): class DisplayMixin: """Base class for the Display Component and the Monitor Device.""" - size = Column(Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True) + + size = Column( + Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True + ) size.comment = """The size of the monitor in inches.""" technology = Column(DBEnum(DisplayTech)) technology.comment = """The technology the monitor uses to display the image. """ - resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000), - nullable=True) + resolution_width = Column( + SmallInteger, check_range('resolution_width', 10, 20000), nullable=True + ) resolution_width.comment = """The maximum horizontal resolution the monitor can natively support in pixels. """ - resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000), - nullable=True) + resolution_height = Column( + SmallInteger, check_range('resolution_height', 10, 20000), nullable=True + ) resolution_height.comment = """The maximum vertical resolution the monitor can natively support in pixels. """ @@ -622,8 +723,12 @@ class DisplayMixin: def __str__(self) -> str: if self.size: - return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self) - return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(self) + return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format( + self + ) + return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format( + self + ) def __format__(self, format_spec: str) -> str: v = '' @@ -645,6 +750,7 @@ class Computer(Device): Computer is broadly extended by ``Desktop``, ``Laptop``, and ``Server``. The property ``chassis`` defines it more granularly. """ + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) chassis = Column(DBEnum(ComputerChassis), nullable=True) chassis.comment = """The physical form of the computer. @@ -652,16 +758,18 @@ class Computer(Device): It is a subset of the Linux definition of DMI / DMI decode. """ amount = Column(Integer, check_range('amount', min=0, max=100), default=0) - owner_id = db.Column(UUID(as_uuid=True), - db.ForeignKey(User.id), - nullable=False, - default=lambda: g.user.id) + owner_id = db.Column( + UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id, + ) # author = db.relationship(User, primaryjoin=owner_id == User.id) - transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False) + transfer_state = db.Column( + IntEnum(TransferState), default=TransferState.Initial, nullable=False + ) transfer_state.comment = TransferState.__doc__ - receiver_id = db.Column(UUID(as_uuid=True), - db.ForeignKey(User.id), - nullable=True) + receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True) receiver = db.relationship(User, primaryjoin=receiver_id == User.id) uuid = db.Column(UUID(as_uuid=True), nullable=True) @@ -685,22 +793,30 @@ class Computer(Device): @property def ram_size(self) -> int: """The total of RAM memory the computer has.""" - return sum(ram.size or 0 for ram in self.components if isinstance(ram, RamModule)) + return sum( + ram.size or 0 for ram in self.components if isinstance(ram, RamModule) + ) @property def data_storage_size(self) -> int: """The total of data storage the computer has.""" - return sum(ds.size or 0 for ds in self.components if isinstance(ds, DataStorage)) + return sum( + ds.size or 0 for ds in self.components if isinstance(ds, DataStorage) + ) @property def processor_model(self) -> str: """The model of one of the processors of the computer.""" - return next((p.model for p in self.components if isinstance(p, Processor)), None) + return next( + (p.model for p in self.components if isinstance(p, Processor)), None + ) @property def graphic_card_model(self) -> str: """The model of one of the graphic cards of the computer.""" - return next((p.model for p in self.components if isinstance(p, GraphicCard)), None) + return next( + (p.model for p in self.components if isinstance(p, GraphicCard)), None + ) @property def network_speeds(self) -> List[int]: @@ -725,16 +841,18 @@ class Computer(Device): it is not None. """ return set( - privacy for privacy in - (hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage)) + privacy + for privacy in ( + hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage) + ) if privacy ) @property def external_document_erasure(self): - """Returns the external ``DataStorage`` proof of erasure. - """ + """Returns the external ``DataStorage`` proof of erasure.""" from ereuse_devicehub.resources.action.models import DataWipe + urls = set() try: ev = self.last_action_of(DataWipe) @@ -757,8 +875,11 @@ class Computer(Device): if not self.hid: return components = self.components if components_snap is None else components_snap - macs_network = [c.serial_number for c in components - if c.type == 'NetworkAdapter' and c.serial_number is not None] + macs_network = [ + c.serial_number + for c in components + if c.type == 'NetworkAdapter' and c.serial_number is not None + ] macs_network.sort() mac = macs_network[0] if macs_network else '' if not mac or mac in self.hid: @@ -824,9 +945,13 @@ class Mobile(Device): """ ram_size = db.Column(db.Integer, check_range('ram_size', min=128, max=36000)) ram_size.comment = """The total of RAM of the device in MB.""" - data_storage_size = db.Column(db.Integer, check_range('data_storage_size', 0, 10 ** 8)) + data_storage_size = db.Column( + db.Integer, check_range('data_storage_size', 0, 10**8) + ) data_storage_size.comment = """The total of data storage of the device in MB""" - display_size = db.Column(db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0)) + display_size = db.Column( + db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0) + ) display_size.comment = """The total size of the device screen""" @validates('imei') @@ -856,21 +981,24 @@ class Cellphone(Mobile): class Component(Device): """A device that can be inside another device.""" + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) parent_id = Column(BigInteger, ForeignKey(Computer.id)) - parent = relationship(Computer, - backref=backref('components', - lazy=True, - cascade=CASCADE_DEL, - order_by=lambda: Component.id, - collection_class=OrderedSet), - primaryjoin=parent_id == Computer.id) - - __table_args__ = ( - db.Index('parent_index', parent_id, postgresql_using='hash'), + parent = relationship( + Computer, + backref=backref( + 'components', + lazy=True, + cascade=CASCADE_DEL, + order_by=lambda: Component.id, + collection_class=OrderedSet, + ), + primaryjoin=parent_id == Computer.id, ) + __table_args__ = (db.Index('parent_index', parent_id, postgresql_using='hash'),) + def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component': """Gets a component that: @@ -882,11 +1010,16 @@ class Component(Device): when looking for similar ones. """ assert self.hid is None, 'Don\'t use this method with a component that has HID' - component = self.__class__.query \ - .filter_by(parent=parent, hid=None, owner_id=self.owner_id, - **self.physical_properties) \ - .filter(~Component.id.in_(blacklist)) \ + component = ( + self.__class__.query.filter_by( + parent=parent, + hid=None, + owner_id=self.owner_id, + **self.physical_properties, + ) + .filter(~Component.id.in_(blacklist)) .first() + ) if not component: raise ResourceNotFound(self.type) return component @@ -909,7 +1042,8 @@ class GraphicCard(JoinedComponentTableMixin, Component): class DataStorage(JoinedComponentTableMixin, Component): """A device that stores information.""" - size = Column(Integer, check_range('size', min=1, max=10 ** 8)) + + size = Column(Integer, check_range('size', min=1, max=10**8)) size.comment = """The size of the data-storage in MB.""" interface = Column(DBEnum(DataStorageInterface)) @@ -920,6 +1054,7 @@ class DataStorage(JoinedComponentTableMixin, Component): This is, the last erasure performed to the data storage. """ from ereuse_devicehub.resources.action.models import EraseBasic + try: ev = self.last_action_of(EraseBasic) except LookupError: @@ -934,9 +1069,9 @@ class DataStorage(JoinedComponentTableMixin, Component): @property def external_document_erasure(self): - """Returns the external ``DataStorage`` proof of erasure. - """ + """Returns the external ``DataStorage`` proof of erasure.""" from ereuse_devicehub.resources.action.models import DataWipe + try: ev = self.last_action_of(DataWipe) return ev.document.url.to_text() @@ -986,6 +1121,7 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component): class Processor(JoinedComponentTableMixin, Component): """The CPU.""" + speed = Column(Float, check_range('speed', 0.1, 15)) speed.comment = """The regular CPU speed.""" cores = Column(SmallInteger, check_range('cores', 1, 10)) @@ -1000,6 +1136,7 @@ class Processor(JoinedComponentTableMixin, Component): class RamModule(JoinedComponentTableMixin, Component): """A stick of RAM.""" + size = Column(SmallInteger, check_range('size', min=128, max=17000)) size.comment = """The capacity of the RAM stick.""" speed = Column(SmallInteger, check_range('speed', min=100, max=10000)) @@ -1017,6 +1154,7 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component): mobiles, smart-watches, and so on; excluding ``ComputerMonitor`` and ``TelevisionSet``. """ + pass @@ -1032,14 +1170,16 @@ class Battery(JoinedComponentTableMixin, Component): @property def capacity(self) -> float: - """The quantity of """ + """The quantity of""" from ereuse_devicehub.resources.action.models import MeasureBattery + real_size = self.last_action_of(MeasureBattery).size return real_size / self.size if real_size and self.size else None class Camera(Component): """The camera of a device.""" + focal_length = db.Column(db.SmallInteger) video_height = db.Column(db.SmallInteger) video_width = db.Column(db.Integer) @@ -1052,6 +1192,7 @@ class Camera(Component): class ComputerAccessory(Device): """Computer peripherals and similar accessories.""" + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) pass @@ -1074,6 +1215,7 @@ class MemoryCardReader(ComputerAccessory): class Networking(NetworkMixin, Device): """Routers, switches, hubs...""" + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) @@ -1119,6 +1261,7 @@ class Microphone(Sound): class Video(Device): """Devices related to video treatment.""" + pass @@ -1132,6 +1275,7 @@ class Videoconference(Video): class Cooking(Device): """Cooking devices.""" + pass @@ -1183,6 +1327,7 @@ class Manufacturer(db.Model): Ideally users should use the names from this list when submitting devices. """ + name = db.Column(CIText(), primary_key=True) name.comment = """The normalized name of the manufacturer.""" url = db.Column(URL(), unique=True) @@ -1193,7 +1338,7 @@ class Manufacturer(db.Model): __table_args__ = ( # from https://niallburkley.com/blog/index-columns-for-like-in-postgres/ db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'), - {'schema': 'common'} + {'schema': 'common'}, ) @classmethod @@ -1203,10 +1348,7 @@ class Manufacturer(db.Model): #: Dialect used to write the CSV with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f: - cursor.copy_expert( - 'COPY common.manufacturer FROM STDIN (FORMAT csv)', - f - ) + cursor.copy_expert('COPY common.manufacturer FROM STDIN (FORMAT csv)', f) listener_reset_field_updated_in_actual_time(Device) @@ -1218,6 +1360,7 @@ def create_code_tag(mapper, connection, device): this tag is the same of devicehub_id. """ from ereuse_devicehub.resources.tag.model import Tag + if isinstance(device, Computer): tag = Tag(device_id=device.id, id=device.devicehub_id) db.session.add(tag) 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/resources/user/models.py b/ereuse_devicehub/resources/user/models.py index 70f14e00..5eadb21d 100644 --- a/ereuse_devicehub/resources/user/models.py +++ b/ereuse_devicehub/resources/user/models.py @@ -2,37 +2,44 @@ from uuid import uuid4 from flask import current_app as app from flask_login import UserMixin -from sqlalchemy import Column, Boolean, BigInteger, Sequence +from sqlalchemy import BigInteger, Boolean, Column, Sequence from sqlalchemy.dialects.postgresql import UUID from sqlalchemy_utils import EmailType, PasswordType from teal.db import IntEnum from ereuse_devicehub.db import db +from ereuse_devicehub.resources.enums import SessionType from ereuse_devicehub.resources.inventory.model import Inventory from ereuse_devicehub.resources.models import STR_SIZE, Thing -from ereuse_devicehub.resources.enums import SessionType class User(UserMixin, Thing): __table_args__ = {'schema': 'common'} id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True) email = Column(EmailType, nullable=False, unique=True) - password = Column(PasswordType(max_length=STR_SIZE, - onload=lambda **kwargs: dict( - schemes=app.config['PASSWORD_SCHEMES'], - **kwargs - ))) + password = Column( + PasswordType( + max_length=STR_SIZE, + onload=lambda **kwargs: dict( + schemes=app.config['PASSWORD_SCHEMES'], **kwargs + ), + ) + ) token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False) active = Column(Boolean, default=True, nullable=False) phantom = Column(Boolean, default=False, nullable=False) - inventories = db.relationship(Inventory, - backref=db.backref('users', lazy=True, collection_class=set), - secondary=lambda: UserInventory.__table__, - collection_class=set) + inventories = db.relationship( + Inventory, + backref=db.backref('users', lazy=True, collection_class=set), + secondary=lambda: UserInventory.__table__, + collection_class=set, + ) # todo set restriction that user has, at least, one active db - def __init__(self, email, password=None, inventories=None, active=True, phantom=False) -> None: + def __init__( + self, email, password=None, inventories=None, active=True, phantom=False + ) -> None: """Creates an user. :param email: :param password: @@ -44,8 +51,13 @@ class User(UserMixin, Thing): create during the trade actions """ inventories = inventories or {Inventory.current} - super().__init__(email=email, password=password, inventories=inventories, - active=active, phantom=phantom) + super().__init__( + email=email, + password=password, + inventories=inventories, + active=active, + phantom=phantom, + ) def __repr__(self) -> str: return ''.format(self) @@ -73,8 +85,8 @@ class User(UserMixin, Thing): @property def get_full_name(self): - # TODO(@slamora) create first_name & last_name fields and use - # them to generate user full name + # TODO(@slamora) create first_name & last_name fields??? + # needs to be discussed related to Agent <--> User concepts return self.email def check_password(self, password): @@ -84,9 +96,12 @@ class User(UserMixin, Thing): class UserInventory(db.Model): """Relationship between users and their inventories.""" + __table_args__ = {'schema': 'common'} user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id), primary_key=True) - inventory_id = db.Column(db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True) + inventory_id = db.Column( + db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True + ) class Session(Thing): @@ -96,9 +111,11 @@ class Session(Thing): token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False) type = Column(IntEnum(SessionType), default=SessionType.Internal, nullable=False) user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id)) - user = db.relationship(User, - backref=db.backref('sessions', lazy=True, collection_class=set), - collection_class=set) + user = db.relationship( + User, + backref=db.backref('sessions', lazy=True, collection_class=set), + collection_class=set, + ) def __str__(self) -> str: return '{0.token}'.format(self) diff --git a/ereuse_devicehub/static/css/devicehub.css b/ereuse_devicehub/static/css/devicehub.css new file mode 100644 index 00000000..e6ae1893 --- /dev/null +++ b/ereuse_devicehub/static/css/devicehub.css @@ -0,0 +1,25 @@ +/** +* eReuse CSS +*/ + +/*-------------------------------------------------------------- +# LotsSelector +--------------------------------------------------------------*/ + +#dropDownLotsSelector { + max-height: 500px; +} + +#dropDownLotsSelector>ul#LotsSelector { + list-style-type: none; + margin: 0; + padding: 0; + min-width: max-content; + max-height: 380px; + overflow-y: auto; +} + +#dropDownLotsSelector #ApplyDeviceLots { + padding-top: 0px; + padding-bottom: 5px; +} diff --git a/ereuse_devicehub/static/css/style.css b/ereuse_devicehub/static/css/style.css index 8e263853..1766a804 100644 --- a/ereuse_devicehub/static/css/style.css +++ b/ereuse_devicehub/static/css/style.css @@ -1,10 +1,10 @@ -/** -* Template Name: NiceAdmin - v2.2.0 -* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ -* Author: BootstrapMade.com -* License: https://bootstrapmade.com/license/ -*/ - +/** +* Template Name: NiceAdmin - v2.2.0 +* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/ +* Author: BootstrapMade.com +* License: https://bootstrapmade.com/license/ +*/ + /*-------------------------------------------------------------- # General --------------------------------------------------------------*/ @@ -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% { @@ -1081,4 +1114,4 @@ h1, h2, h3, h4, h5, h6 { text-align: center; font-size: 13px; color: #012970; -} \ No newline at end of file +} 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 ee98a08f..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() { - var 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; }, @@ -15,7 +15,7 @@ const Api = { * @returns full detailed device list */ async get_devices(ids) { - var request = await this.doRequest(API_URLS.devices + '?filter={"id": [' + ids.toString() + ']}', "GET", null); + const request = await this.doRequest(`${API_URLS.devices }?filter={"id": [${ ids.toString() }]}`, "GET", null); if (request != undefined) return request.items; throw request; }, @@ -26,7 +26,7 @@ const Api = { * @returns full detailed device list */ async search_device(id) { - var request = await this.doRequest(API_URLS.devices + '?filter={"devicehub_id": ["' + id + '"]}', "GET", null) + const request = await this.doRequest(`${API_URLS.devices }?filter={"devicehub_id": ["${ id }"]}`, "GET", null) if (request != undefined) return request.items throw request }, @@ -37,8 +37,8 @@ const Api = { * @param {number[]} listDevices list devices id */ async devices_add(lotID, listDevices) { - var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&"); - return await Api.doRequest(queryURL, "POST", null); + const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`; + return Api.doRequest(queryURL, "POST", null); }, /** @@ -47,8 +47,8 @@ const Api = { * @param {number[]} listDevices list devices id */ async devices_remove(lotID, listDevices) { - var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&"); - return await Api.doRequest(queryURL, "DELETE", null); + const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`; + return Api.doRequest(queryURL, "DELETE", null); }, /** @@ -59,13 +59,13 @@ const Api = { * @returns */ async doRequest(url, type, body) { - var result; + let result; try { result = await $.ajax({ - url: url, - type: type, + url, + type, headers: { "Authorization": API_URLS.Auth_Token }, - body: body + body }); return result; } catch (error) { diff --git a/ereuse_devicehub/static/js/create_device.js b/ereuse_devicehub/static/js/create_device.js index 1c9e0655..a1b609b0 100644 --- a/ereuse_devicehub/static/js/create_device.js +++ b/ereuse_devicehub/static/js/create_device.js @@ -1,15 +1,15 @@ -$(document).ready(function() { +$(document).ready(() => { $("#type").on("change", deviceInputs); deviceInputs(); }) function deviceInputs() { - if ($("#type").val() == 'Monitor') { + if ($("#type").val() == "Monitor") { $("#screen").show(); $("#resolution").show(); $("#imei").hide(); $("#meid").hide(); - } else if (['Smartphone', 'Cellphone', 'Tablet'].includes($("#type").val())) { + } else if (["Smartphone", "Cellphone", "Tablet"].includes($("#type").val())) { $("#screen").hide(); $("#resolution").hide(); $("#imei").show(); diff --git a/ereuse_devicehub/static/js/main.js b/ereuse_devicehub/static/js/main.js index 5eaec3ea..996d8b23 100644 --- a/ereuse_devicehub/static/js/main.js +++ b/ereuse_devicehub/static/js/main.js @@ -14,9 +14,9 @@ el = el.trim() if (all) { return [...document.querySelectorAll(el)] - } else { - return document.querySelector(el) } + return document.querySelector(el) + } /** @@ -34,103 +34,101 @@ * Easy on scroll event listener */ const onscroll = (el, listener) => { - el.addEventListener('scroll', listener) + el.addEventListener("scroll", listener) } /** * Sidebar toggle */ - if (select('.toggle-sidebar-btn')) { - on('click', '.toggle-sidebar-btn', function (e) { - select('body').classList.toggle('toggle-sidebar') + if (select(".toggle-sidebar-btn")) { + on("click", ".toggle-sidebar-btn", (e) => { + select("body").classList.toggle("toggle-sidebar") }) } /** * Search bar toggle */ - if (select('.search-bar-toggle')) { - on('click', '.search-bar-toggle', function (e) { - select('.search-bar').classList.toggle('search-bar-show') + if (select(".search-bar-toggle")) { + on("click", ".search-bar-toggle", (e) => { + select(".search-bar").classList.toggle("search-bar-show") }) } /** * Navbar links active state on scroll */ - let navbarlinks = select('#navbar .scrollto', true) + const navbarlinks = select("#navbar .scrollto", true) const navbarlinksActive = () => { - let position = window.scrollY + 200 + const position = window.scrollY + 200 navbarlinks.forEach(navbarlink => { if (!navbarlink.hash) return - let section = select(navbarlink.hash) + const section = select(navbarlink.hash) if (!section) return if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) { - navbarlink.classList.add('active') + navbarlink.classList.add("active") } else { - navbarlink.classList.remove('active') + navbarlink.classList.remove("active") } }) } - window.addEventListener('load', navbarlinksActive) + window.addEventListener("load", navbarlinksActive) onscroll(document, navbarlinksActive) /** * Toggle .header-scrolled class to #header when page is scrolled */ - let selectHeader = select('#header') + const selectHeader = select("#header") if (selectHeader) { const headerScrolled = () => { if (window.scrollY > 100) { - selectHeader.classList.add('header-scrolled') + selectHeader.classList.add("header-scrolled") } else { - selectHeader.classList.remove('header-scrolled') + selectHeader.classList.remove("header-scrolled") } } - window.addEventListener('load', headerScrolled) + window.addEventListener("load", headerScrolled) onscroll(document, headerScrolled) } /** * Back to top button */ - let backtotop = select('.back-to-top') + const backtotop = select(".back-to-top") if (backtotop) { const toggleBacktotop = () => { if (window.scrollY > 100) { - backtotop.classList.add('active') + backtotop.classList.add("active") } else { - backtotop.classList.remove('active') + backtotop.classList.remove("active") } } - window.addEventListener('load', toggleBacktotop) + window.addEventListener("load", toggleBacktotop) onscroll(document, toggleBacktotop) } /** * Initiate tooltips */ - var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) - var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl) - }) + const tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]")) + const tooltipList = tooltipTriggerList.map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)) /** * Initiate quill editors */ - if (select('.quill-editor-default')) { - new Quill('.quill-editor-default', { - theme: 'snow' + if (select(".quill-editor-default")) { + new Quill(".quill-editor-default", { + theme: "snow" }); } - if (select('.quill-editor-bubble')) { - new Quill('.quill-editor-bubble', { - theme: 'bubble' + if (select(".quill-editor-bubble")) { + new Quill(".quill-editor-bubble", { + theme: "bubble" }); } - if (select('.quill-editor-full')) { + if (select(".quill-editor-full")) { new Quill(".quill-editor-full", { modules: { toolbar: [ @@ -181,24 +179,24 @@ /** * Initiate Bootstrap validation check */ - var needsValidation = document.querySelectorAll('.needs-validation') + const needsValidation = document.querySelectorAll(".needs-validation") Array.prototype.slice.call(needsValidation) - .forEach(function (form) { - form.addEventListener('submit', function (event) { + .forEach((form) => { + form.addEventListener("submit", (event) => { if (!form.checkValidity()) { event.preventDefault() event.stopPropagation() } - form.classList.add('was-validated') + form.classList.add("was-validated") }, false) }) /** * Initiate Datatables */ - const datatables = select('.datatable', true) + const datatables = select(".datatable", true) datatables.forEach(datatable => { new simpleDatatables.DataTable(datatable); }) @@ -206,45 +204,17 @@ /** * Autoresize echart charts */ - const mainContainer = select('#main'); + const mainContainer = select("#main"); if (mainContainer) { setTimeout(() => { - new ResizeObserver(function () { - select('.echart', true).forEach(getEchart => { + new ResizeObserver(() => { + select(".echart", true).forEach(getEchart => { echarts.getInstanceByDom(getEchart).resize(); }) }).observe(mainContainer); }, 200); } - /** - * Select all functionality - */ - var btnSelectAll = document.getElementById("SelectAllBTN"); - var tableListCheckboxes = document.querySelectorAll(".deviceSelect"); - - function itemListCheckChanged(event) { - let isAllChecked = Array.from(tableListCheckboxes).map(itm => itm.checked); - if (isAllChecked.every(bool => bool == true)) { - btnSelectAll.checked = true; - btnSelectAll.indeterminate = false; - } else if (isAllChecked.every(bool => bool == false)) { - btnSelectAll.checked = false; - btnSelectAll.indeterminate = false; - } else { - btnSelectAll.indeterminate = true; - } - } - - tableListCheckboxes.forEach(item => { - item.addEventListener("click", itemListCheckChanged); - }) - - btnSelectAll.addEventListener("click", event => { - let checkedState = event.target.checked; - tableListCheckboxes.forEach(ckeckbox => ckeckbox.checked = checkedState); - }) - /** * Avoid hide dropdown when user clicked inside */ @@ -256,23 +226,23 @@ * Search form functionality */ window.addEventListener("DOMContentLoaded", () => { - var searchForm = document.getElementById("SearchForm") - var inputSearch = document.querySelector("#SearchForm > input") - var doSearch = true + const searchForm = document.getElementById("SearchForm") + const inputSearch = document.querySelector("#SearchForm > input") + const doSearch = true searchForm.addEventListener("submit", (event) => { event.preventDefault(); }) let timeoutHandler = setTimeout(() => { }, 1) - let dropdownList = document.getElementById("dropdown-search-list") - let defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML + const dropdownList = document.getElementById("dropdown-search-list") + const defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML inputSearch.addEventListener("input", (e) => { clearTimeout(timeoutHandler) - let searchText = e.target.value - if (searchText == '') { + const searchText = e.target.value + if (searchText == "") { document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch; return } @@ -315,7 +285,7 @@ const device = devices[i]; // See: ereuse_devicehub/resources/device/models.py - var verboseName = `${device.type} ${device.manufacturer} ${device.model}` + const verboseName = `${device.type} ${device.manufacturer} ${device.model}` const templateString = `
  • diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index bf2fdf43..848c72e4 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -1,7 +1,7 @@ -$(document).ready(function() { - var show_allocate_form = $("#allocateModal").data('show-action-form'); - var show_datawipe_form = $("#datawipeModal").data('show-action-form'); - var show_trade_form = $("#tradeLotModal").data('show-action-form'); +$(document).ready(() => { + const show_allocate_form = $("#allocateModal").data("show-action-form"); + const show_datawipe_form = $("#datawipeModal").data("show-action-form"); + const show_trade_form = $("#tradeLotModal").data("show-action-form"); if (show_allocate_form != "None") { $("#allocateModal .btn-primary").show(); newAllocate(show_allocate_form); @@ -17,8 +17,119 @@ $(document).ready(function() { // $('#selectLot').selectpicker(); }) +class TableController { + static #tableRows = () => table.activeRows.length > 0 ? table.activeRows : []; + + static #tableRowsPage = () => table.pages[table.rows().dt.currentPage - 1]; + + /** + * @returns Selected inputs from device list + */ + static getSelectedDevices() { + if (this.#tableRows() == undefined) return []; + return this.#tableRows() + .filter(element => element.querySelector("input").checked) + .map(element => element.querySelector("input")) + } + + /** + * @returns Selected inputs in current page from device list + */ + static getAllSelectedDevicesInCurrentPage() { + if (this.#tableRowsPage() == undefined) return []; + return this.#tableRowsPage() + .filter(element => element.querySelector("input").checked) + .map(element => element.querySelector("input")) + } + + /** + * @returns All inputs from device list + */ + static getAllDevices() { + if (this.#tableRows() == undefined) return []; + return this.#tableRows() + .map(element => element.querySelector("input")) + } + + /** + * @returns All inputs from current page in device list + */ + static getAllDevicesInCurrentPage() { + if (this.#tableRowsPage() == undefined) return []; + return this.#tableRowsPage() + .map(element => element.querySelector("input")) + } + + /** + * + * @param {HTMLElement} DOMElements + * @returns Procesed input atributes to an Object class + */ + static ProcessTR(DOMElements) { + return DOMElements.map(element => { + const info = {} + info.checked = element.checked + Object.values(element.attributes).forEach(attrib => { info[attrib.nodeName.replace(/-/g, "_")] = attrib.nodeValue }) + return info + }) + } +} + +/** + * Select all functionality + */ +window.addEventListener("DOMContentLoaded", () => { + 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); + + 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") + } + } + + TableController.getAllDevices().forEach(item => { + item.addEventListener("click", itemListCheckChanged); + }) + + 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 + table.on("datatable.page", () => itemListCheckChanged()); + table.on("datatable.perpage", () => itemListCheckChanged()); + table.on("datatable.update", () => itemListCheckChanged()); +}) + function deviceSelect() { - var devices_count = $(".deviceSelect").filter(':checked').length; + const devices_count = TableController.getSelectedDevices().length; get_device_list(); if (devices_count == 0) { $("#addingLotModal .pol").show(); @@ -60,7 +171,7 @@ function deviceSelect() { } function removeLot() { - var devices = $(".deviceSelect"); + const devices = TableController.getAllDevices(); if (devices.length > 0) { $("#btnRemoveLots .text-danger").show(); } else { @@ -70,10 +181,10 @@ function removeLot() { } function removeTag() { - var devices = $(".deviceSelect").filter(':checked'); - var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); + const devices = TableController.getSelectedDevices(); + const devices_id = devices.map(dev => dev.data); if (devices_id.length == 1) { - var url = "/inventory/tag/devices/"+devices_id[0]+"/del/"; + const url = `/inventory/tag/devices/${devices_id[0]}/del/`; window.location.href = url; } else { $("#unlinkTagAlertModal").click(); @@ -81,8 +192,8 @@ function removeTag() { } function addTag() { - var devices = $(".deviceSelect").filter(':checked'); - var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); + const devices = TableController.getSelectedDevices(); + const devices_id = devices.map(dev => dev.data); if (devices_id.length == 1) { $("#addingTagModal .pol").hide(); $("#addingTagModal .btn-primary").show(); @@ -95,20 +206,20 @@ function addTag() { } function newTrade(action) { - var title = "Trade " - var user_to = $("#user_to").data("email"); - var user_from = $("#user_from").data("email"); - if (action == 'user_from') { - title = 'Trade Incoming'; - $("#user_to").attr('readonly', 'readonly'); - $("#user_from").prop('readonly', false); - $("#user_from").val(''); + let title = "Trade " + const user_to = $("#user_to").data("email"); + const user_from = $("#user_from").data("email"); + if (action == "user_from") { + title = "Trade Incoming"; + $("#user_to").attr("readonly", "readonly"); + $("#user_from").prop("readonly", false); + $("#user_from").val(""); $("#user_to").val(user_to); - } else if (action == 'user_to') { - title = 'Trade Outgoing'; - $("#user_from").attr('readonly', 'readonly'); - $("#user_to").prop('readonly', false); - $("#user_to").val(''); + } else if (action == "user_to") { + title = "Trade Outgoing"; + $("#user_from").attr("readonly", "readonly"); + $("#user_to").prop("readonly", false); + $("#user_to").val(""); $("#user_from").val(user_from); } $("#tradeLotModal #title-action").html(title); @@ -137,44 +248,45 @@ function newDataWipe(action) { } function get_device_list() { - var devices = $(".deviceSelect").filter(':checked'); + const devices = TableController.getSelectedDevices(); /* Insert the correct count of devices in actions form */ - var devices_count = devices.length; + const devices_count = devices.length; $("#datawipeModal .devices-count").html(devices_count); $("#allocateModal .devices-count").html(devices_count); $("#actionModal .devices-count").html(devices_count); /* Insert the correct value in the input devicesList */ - var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(","); - $.map($(".devicesList"), function(x) { + const devices_id = $.map(devices, (x) => $(x).attr("data")).join(","); + $.map($(".devicesList"), (x) => { $(x).val(devices_id); }); /* Create a list of devices for human representation */ - var computer = { + const computer = { "Desktop": "", "Laptop": "", }; - list_devices = devices.map(function (x) { - var typ = $(devices[x]).data("device-type"); - var manuf = $(devices[x]).data("device-manufacturer"); - var dhid = $(devices[x]).data("device-dhid"); + + list_devices = devices.map((x) => { + let typ = $(x).data("device-type"); + const manuf = $(x).data("device-manufacturer"); + const dhid = $(x).data("device-dhid"); if (computer[typ]) { typ = computer[typ]; }; - return typ + " " + manuf + " " + dhid; + return `${typ} ${manuf} ${dhid}`; }); - description = $.map(list_devices, function(x) { return x }).join(", "); + description = $.map(list_devices, (x) => x).join(", "); $(".enumeration-devices").html(description); } function export_file(type_file) { - var devices = $(".deviceSelect").filter(':checked'); - var devices_id = $.map(devices, function(x) { return $(x).attr('data-device-dhid')}).join(","); - if (devices_id){ - var url = "/inventory/export/"+type_file+"/?ids="+devices_id; + const devices = TableController.getSelectedDevices(); + const devices_id = $.map(devices, (x) => $(x).attr("data-device-dhid")).join(","); + if (devices_id) { + const url = `/inventory/export/${type_file}/?ids=${devices_id}`; window.location.href = url; } else { $("#exportAlertModal").click(); @@ -194,40 +306,28 @@ async function processSelectedDevices() { /** * Manage the actions that will be performed when applying the changes - * @param {*} ev event (Should be a checkbox type) - * @param {string} lotID lot id - * @param {number} deviceID device id + * @param {EventSource} ev event (Should be a checkbox type) + * @param {Lot} lot lot id + * @param {Device[]} selectedDevices device id */ - manage(event, lotID, deviceListID) { + manage(event, lot, selectedDevices) { event.preventDefault(); - const indeterminate = event.srcElement.indeterminate; - const checked = !event.srcElement.checked; + const lotID = lot.id; + const srcElement = event.srcElement.parentElement.children[0] + const checked = !srcElement.checked; - var found = this.list.filter(list => list.lotID == lotID)[0]; - var foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1; + const found = this.list.filter(list => list.lot.id == lotID)[0]; if (checked) { - if (found != undefined && found.type == "Remove") { - if (found.isFromIndeterminate == true) { - found.type = "Add"; - this.list[foundIndex] = found; - } else { - this.list = this.list.filter(list => list.lotID != lotID); - } + if (found && found.type == "Remove") { + found.type = "Add"; } else { - this.list.push({ type: "Add", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); + this.list.push({ type: "Add", lot, devices: selectedDevices }); } + } else if (found && found.type == "Add") { + found.type = "Remove"; } else { - if (found != undefined && found.type == "Add") { - if (found.isFromIndeterminate == true) { - found.type = "Remove"; - this.list[foundIndex] = found; - } else { - this.list = this.list.filter(list => list.lotID != lotID); - } - } else { - this.list.push({ type: "Remove", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate }); - } + this.list.push({ type: "Remove", lot, devices: selectedDevices }); } if (this.list.length > 0) { @@ -244,10 +344,10 @@ async function processSelectedDevices() { * @param {boolean} isError defines if a toast is a error */ notifyUser(title, toastText, isError) { - let toast = document.createElement("div"); - toast.classList = "alert alert-dismissible fade show " + (isError ? "alert-danger" : "alert-success"); + const toast = document.createElement("div"); + toast.classList = `alert alert-dismissible fade show ${isError ? "alert-danger" : "alert-success"}`; toast.attributes["data-autohide"] = !isError; - toast.attributes["role"] = "alert"; + toast.attributes.role = "alert"; toast.style = "margin-left: auto; width: fit-content;"; toast.innerHTML = `${title}`; if (toastText && toastText.length > 0) { @@ -265,21 +365,23 @@ async function processSelectedDevices() { * Get actions and execute call request to add or remove devices from lots */ doActions() { - var requestCount = 0; // This is for count all requested api count, to perform reRender of table device list + let requestCount = 0; // This is for count all requested api count, to perform reRender of table device list this.list.forEach(async action => { if (action.type == "Add") { try { - await Api.devices_add(action.lotID, action.devices); - this.notifyUser("Devices sucefully aded to selected lot/s", "", false); + const devicesIDs = action.devices.filter(dev => !action.lot.devices.includes(dev.id)).map(dev => dev.id) + await Api.devices_add(action.lot.id, devicesIDs); + this.notifyUser("Devices sucefully added to selected lot/s", "", false); } catch (error) { this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true); } } else if (action.type == "Remove") { try { - await Api.devices_remove(action.lotID, action.devices); + const devicesIDs = action.devices.filter(dev => action.lot.devices.includes(dev.id)).map(dev => dev.id) + await Api.devices_remove(action.lot.id, devicesIDs); this.notifyUser("Devices sucefully removed from selected lot/s", "", false); } catch (error) { - this.notifyUser("Fail to remove devices from selected lot/s", error.responseJSON.message, true); + this.notifyUser("Failed to remove devices from selected lot/s", error.responseJSON.message, true); } } requestCount += 1 @@ -288,6 +390,7 @@ async function processSelectedDevices() { this.list = []; } }) + $("#confirmLotsModal").modal("hide"); // Hide dialog when click "Save changes" document.getElementById("dropDownLotsSelector").classList.remove("show"); } @@ -295,90 +398,163 @@ async function processSelectedDevices() { * Re-render list in table */ async reRenderTable() { - var newRequest = await Api.doRequest(window.location) + const newRequest = await Api.doRequest(window.location) - var tmpDiv = document.createElement("div") + const tmpDiv = document.createElement("div") tmpDiv.innerHTML = newRequest - var oldTable = Array.from(document.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) - var newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) + const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value) - for (let i = 0; i < oldTable.length; i++) { - if (!newTable.includes(oldTable[i])) { - // variable from device_list.html --> See: ereuse_devicehub\templates\inventory\device_list.html (Ln: 411) - table.rows().remove(i) + // 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 selectAllBTN = document.getElementById("SelectAllBTN"); + selectAllBTN.checked = false; + selectAllBTN.indeterminate = false; } } - var eventClickActions; + let eventClickActions; /** * Generates a list item with a correspondient checkbox state - * @param {String} lotID - * @param {String} lotName - * @param {Array} selectedDevicesIDs - * @param {HTMLElement} target + * @param {Object} lot Lot model server + * @param {Device[]} selectedDevices list selected devices + * @param {HTMLElement} elementTarget + * @param {Action[]} actions */ - function templateLot(lotID, lot, selectedDevicesIDs, elementTarget, actions) { + function templateLot(lot, selectedDevices, elementTarget, actions) { elementTarget.innerHTML = "" + const { id, name, state } = lot; - var htmlTemplate = ` - `; + const htmlTemplate = ` + `; - var existLotList = selectedDevicesIDs.map(selected => lot.devices.includes(selected)); - - var doc = document.createElement('li'); + const doc = document.createElement("li"); doc.innerHTML = htmlTemplate; - if (selectedDevicesIDs.length <= 0) { - doc.children[0].disabled = true; - } else if (existLotList.every(value => value == true)) { - doc.children[0].checked = true; - } else if (existLotList.every(value => value == false)) { - doc.children[0].checked = false; - } else { - doc.children[0].indeterminate = true; + switch (state) { + case "true": + doc.children[0].checked = true; + break; + case "false": + doc.children[0].checked = false; + break; + case "indetermined": + doc.children[0].indeterminate = true; + break; + default: + console.warn("This shouldn't be happend: Lot without state: ", lot); + break; } - doc.children[0].addEventListener('mouseup', (ev) => actions.manage(ev, lotID, selectedDevicesIDs)); + doc.children[0].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices)); + doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices)); elementTarget.append(doc); } - var listHTML = $("#LotsSelector") + const listHTML = $("#LotsSelector") // Get selected devices - var selectedDevicesIDs = $.map($(".deviceSelect").filter(':checked'), function (x) { return parseInt($(x).attr('data')) }); - if (selectedDevicesIDs.length <= 0) { - listHTML.html('
  • No devices selected
  • '); + const selectedDevicesID = TableController.ProcessTR(TableController.getSelectedDevices()).map(item => item.data) + + if (selectedDevicesID.length <= 0) { + listHTML.html("
  • No devices selected
  • "); return; } // Initialize Actions list, and set checkbox triggers - var actions = new Actions(); + const actions = new Actions(); if (eventClickActions) { document.getElementById("ApplyDeviceLots").removeEventListener(eventClickActions); } - eventClickActions = document.getElementById("ApplyDeviceLots").addEventListener("click", () => actions.doActions()); + + eventClickActions = document.getElementById("ApplyDeviceLots").addEventListener("click", () => { + const modal = $("#confirmLotsModal") + modal.modal({ keyboard: false }) + + let list_changes_html = ""; + // {type: ["Remove" | "Add"], "LotID": string, "devices": number[]} + actions.list.forEach(action => { + let type; + let devices; + if (action.type == "Add") { + type = "success"; + devices = action.devices.filter(dev => !action.lot.devices.includes(dev.id)) // Only show affected devices + } else { + type = "danger"; + devices = action.devices.filter(dev => action.lot.devices.includes(dev.id)) // Only show affected devices + } + list_changes_html += ` +
    +
    ${action.lot.name}
    +
    +

    + ${devices.map(item => { + const name = `${item.type} ${item.manufacturer} ${item.model}` + return `${item.devicehubID}`; + }).join(" ")} +

    +
    +
    `; + }) + + modal.find(".modal-body").html(list_changes_html) + + const el = document.getElementById("SaveAllActions") + const elClone = el.cloneNode(true); + el.parentNode.replaceChild(elClone, el); + elClone.addEventListener("click", () => actions.doActions()) + + modal.modal("show") + + // actions.doActions(); + }); document.getElementById("ApplyDeviceLots").classList.add("disabled"); try { - listHTML.html('
  • ') - var devices = await Api.get_devices(selectedDevicesIDs); - var lots = await Api.get_lots(); + listHTML.html("
  • ") + const selectedDevices = await Api.get_devices(selectedDevicesID); + let lots = await Api.get_lots(); lots = lots.map(lot => { - lot.devices = devices + lot.devices = selectedDevices .filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0) .map(device => parseInt(device.id)); + + switch (lot.devices.length) { + case 0: + lot.state = "false"; + break; + case selectedDevicesID.length: + lot.state = "true"; + break; + default: + lot.state = "indetermined"; + break; + } + return lot; }) - listHTML.html(''); - lots.forEach(lot => templateLot(lot.id, lot, selectedDevicesIDs, listHTML, actions)); + let lotsList = []; + lotsList.push(lots.filter(lot => lot.state == "true").sort((a, b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a, b) => a.name.localeCompare(b.name))); + lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name))); + lotsList = lotsList.flat(); // flat array + + listHTML.html(""); + lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions)); } catch (error) { console.log(error); - listHTML.html('
  • Error feching devices and lots
    (see console for more details)
  • '); + listHTML.html("
  • Error feching devices and lots
    (see console for more details)
  • "); } } diff --git a/ereuse_devicehub/static/js/print.pdf.js b/ereuse_devicehub/static/js/print.pdf.js index 0d6fe6d5..f0b1817c 100644 --- a/ereuse_devicehub/static/js/print.pdf.js +++ b/ereuse_devicehub/static/js/print.pdf.js @@ -1,8 +1,10 @@ $(document).ready(function() { STORAGE_KEY = 'tag-spec-key'; $("#printerType").on("change", change_size); + $(".form-check-input").on("change", change_check); change_size(); - load_size(); + load_settings(); + change_check(); }) function qr_draw(url, id) { @@ -16,27 +18,43 @@ function qr_draw(url, id) { }); } -function save_size() { +function save_settings() { var height = $("#height-tag").val(); var width = $("#width-tag").val(); var sizePreset = $("#printerType").val(); var data = {"height": height, "width": width, "sizePreset": sizePreset}; + data['dhid'] = $("#dhidCheck").prop('checked'); + data['qr'] = $("#qrCheck").prop('checked'); + data['serial_number'] = $("#serialNumberCheck").prop('checked'); + data['manufacturer'] = $("#manufacturerCheck").prop('checked'); + data['model'] = $("#modelCheck").prop('checked'); localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } -function load_size() { +function load_settings() { var data = JSON.parse(localStorage.getItem(STORAGE_KEY)); if (data){ $("#height-tag").val(data.height); $("#width-tag").val(data.width); $("#printerType").val(data.sizePreset); + $("#qrCheck").prop('checked', data.qr); + $("#dhidCheck").prop('checked', data.dhid); + $("#serialNumberCheck").prop('checked', data.serial_number); + $("#manufacturerCheck").prop('checked', data.manufacturer); + $("#modelCheck").prop('checked', data.model); }; } -function reset_size() { +function reset_settings() { localStorage.removeItem(STORAGE_KEY); $("#printerType").val('brotherSmall'); + $("#qrCheck").prop('checked', true); + $("#dhidCheck").prop('checked', true); + $("#serialNumberCheck").prop('checked', false); + $("#manufacturerCheck").prop('checked', false); + $("#modelCheck").prop('checked', false); change_size(); + change_check(); } function change_size() { @@ -50,29 +68,101 @@ function change_size() { } } +function change_check() { + if ($("#dhidCheck").prop('checked')) { + $(".dhid").show(); + } else { + $(".dhid").hide(); + } + if ($("#serialNumberCheck").prop('checked')) { + $(".serial_number").show(); + } else { + $(".serial_number").hide(); + } + if ($("#manufacturerCheck").prop('checked')) { + $(".manufacturer").show(); + } else { + $(".manufacturer").hide(); + } + if ($("#modelCheck").prop('checked')) { + $(".model").show(); + } else { + $(".model").hide(); + } + if ($("#qrCheck").prop('checked')) { + $(".qr").show(); + } else { + $(".qr").hide(); + } +} + function printpdf() { var border = 2; + var line = 5; var height = parseInt($("#height-tag").val()); var width = parseInt($("#width-tag").val()); - img_side = Math.min(height, width) - 2*border; + var img_side = Math.min(height, width) - 2*border; max_tag_side = (Math.max(height, width)/2) + border; if (max_tag_side < img_side) { - max_tag_side = img_side+ 2*border; + max_tag_side = img_side + 2*border; }; min_tag_side = (Math.min(height, width)/2) + border; var last_tag_code = ''; + if ($("#serialNumberCheck").prop('checked')) { + height += line; + }; + if ($("#manufacturerCheck").prop('checked')) { + height += line; + }; + if ($("#modelCheck").prop('checked')) { + height += line; + }; + var pdf = new jsPDF('l', 'mm', [width, height]); $(".tag").map(function(x, y) { if (x != 0){ pdf.addPage(); - console.log(x) }; + var space = line + border; + if ($("#qrCheck").prop('checked')) { + space += img_side; + } var tag = $(y).text(); last_tag_code = tag; - var imgData = $('#'+tag+' img').attr("src"); - pdf.addImage(imgData, 'PNG', border, border, img_side, img_side); - pdf.text(tag, max_tag_side, min_tag_side); + if ($("#qrCheck").prop('checked')) { + var imgData = $('#'+tag+' img').attr("src"); + pdf.addImage(imgData, 'PNG', border, border, img_side, img_side); + }; + + if ($("#dhidCheck").prop('checked')) { + if ($("#qrCheck").prop('checked')) { + pdf.setFontSize(15); + pdf.text(tag, max_tag_side, min_tag_side); + } else { + pdf.setFontSize(15); + pdf.text(tag, border, space); + space += line; + } + }; + if ($("#serialNumberCheck").prop('checked')) { + var sn = $(y).data('serial-number'); + pdf.setFontSize(12); + pdf.text(sn, border, space); + space += line; + }; + if ($("#manufacturerCheck").prop('checked')) { + var sn = $(y).data('manufacturer'); + pdf.setFontSize(12); + pdf.text(sn, border, space); + space += line; + }; + if ($("#modelCheck").prop('checked')) { + var sn = $(y).data('model'); + pdf.setFontSize(8); + pdf.text(sn, border, space); + space += line; + }; }); pdf.save('Tag_'+last_tag_code+'.pdf'); diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base.html b/ereuse_devicehub/templates/ereuse_devicehub/base.html index 8e147828..3b7c94bd 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base.html @@ -29,6 +29,7 @@ + {% endblock main %} diff --git a/ereuse_devicehub/templates/inventory/tag_unlink_device.html b/ereuse_devicehub/templates/inventory/tag_unlink_device.html index 47b5ce8f..af6ed3a6 100644 --- a/ereuse_devicehub/templates/inventory/tag_unlink_device.html +++ b/ereuse_devicehub/templates/inventory/tag_unlink_device.html @@ -18,8 +18,8 @@
    -

    Unlink Tag from Device

    -

    Please enter a code for the tag.

    +

    Unlink Unique Identifier from Device

    +

    Please enter a code for the unique identifier.

    {% if form.form_errors %}

    {% for error in form.form_errors %} @@ -33,10 +33,10 @@ {{ form.csrf_token }}

    - +
    {{ form.tag(class_="form-control") }} -
    Please select tag.
    +
    Please select unique identifier.
    {% if form.tag.errors %}

    diff --git a/ereuse_devicehub/templates/labels/label_detail.html b/ereuse_devicehub/templates/labels/label_detail.html index 75ff3efb..c7e38b96 100644 --- a/ereuse_devicehub/templates/labels/label_detail.html +++ b/ereuse_devicehub/templates/labels/label_detail.html @@ -5,8 +5,8 @@

    Inventory

    @@ -26,7 +26,7 @@
    Type
    -
    {% if tag.provider %}UnNamed Tag{% else %}Named{% endif %}
    +
    {% if tag.provider %}UnNamed Unique Identifier{% else %}Named Unique Identifier{% endif %}
    @@ -43,16 +43,49 @@
    Print Label
    -
    +
    -
    +
    -
    -
    {{ tag.id }}
    +
    +
    + {% if tag.device %} + {{ tag.id }} + {% else %} + {{ tag.id }} + {% endif %} +
    + {% if tag.device %} + + + + {% endif %}
    @@ -84,20 +117,43 @@ mm
    + {% if tag.device %} +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + {% endif %} +
    -
    -
    - Print -
    -
    - Save -
    -
    - Reset -
    -
    -
    diff --git a/ereuse_devicehub/templates/labels/label_list.html b/ereuse_devicehub/templates/labels/label_list.html index 810db5cf..7d184477 100644 --- a/ereuse_devicehub/templates/labels/label_list.html +++ b/ereuse_devicehub/templates/labels/label_list.html @@ -19,18 +19,18 @@
    -