Merge branch 'feature/server-side-render-parser-3021' into feature/list-snapshots-view-#3113
This commit is contained in:
commit
1baeddbd56
|
@ -0,0 +1,4 @@
|
||||||
|
ereuse_devicehub/static/vendor
|
||||||
|
ereuse_devicehub/static/js/print.pdf.js
|
||||||
|
ereuse_devicehub/static/js/qrcode.js
|
||||||
|
*.min.js
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -119,3 +119,11 @@ ENV/
|
||||||
|
|
||||||
# Temporal dir
|
# Temporal dir
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
|
# NPM modules
|
||||||
|
node_modules/
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# ESLint Report
|
||||||
|
eslint_report.json
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"printWidth": 250
|
||||||
|
}
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -9,9 +9,30 @@ ml).
|
||||||
|
|
||||||
## testing
|
## testing
|
||||||
- [added] #219 Add functionality to searchbar (Lots and devices).
|
- [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] #211 Print DHID-QR label for selected devices.
|
||||||
- [changed] #218 Add reactivity to device lots.
|
- [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] #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
|
## [2.0.0] - 2022-03-15
|
||||||
First server render HTML version. Completely rewrites views of angular JS client on flask.
|
First server render HTML version. Completely rewrites views of angular JS client on flask.
|
||||||
|
|
|
@ -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)
|
Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,12 @@ dags-with-materialized-paths-using-postgres-ltree/>`_ you have
|
||||||
a low-level technical implementation of how lots and their
|
a low-level technical implementation of how lots and their
|
||||||
relationships are mapped.
|
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
|
Create lots
|
||||||
***********
|
***********
|
||||||
You create a lot by ``POST /lots/`` a `JSON Lot object <https://
|
You create a lot by ``POST /lots/`` a `JSON Lot object <https://
|
||||||
|
@ -28,7 +34,6 @@ And for devices is all the same:
|
||||||
``POST /lots/<parent-lot-id>/devices/?id=<device-id-1>&id=<device-id-2>``;
|
``POST /lots/<parent-lot-id>/devices/?id=<device-id-1>&id=<device-id-2>``;
|
||||||
idem for removing devices.
|
idem for removing devices.
|
||||||
|
|
||||||
|
|
||||||
Sharing lots
|
Sharing lots
|
||||||
************
|
************
|
||||||
Sharing a lot means giving certain permissions to users, like reading
|
Sharing a lot means giving certain permissions to users, like reading
|
||||||
|
|
|
@ -5,7 +5,8 @@ from flask import Blueprint
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
from flask.views import View
|
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 werkzeug.exceptions import Unauthorized
|
||||||
|
|
||||||
from ereuse_devicehub.auth import Auth
|
from ereuse_devicehub.auth import Auth
|
||||||
|
@ -51,18 +52,22 @@ class InventoryView(LoginMix, SnapshotMix):
|
||||||
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
|
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
|
||||||
self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
|
self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
|
||||||
snapshot_json = self.validate(snapshot_json)
|
snapshot_json = self.validate(snapshot_json)
|
||||||
try:
|
if type(snapshot_json) == Response:
|
||||||
self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot()
|
return snapshot_json
|
||||||
except ValidationError:
|
|
||||||
self.response = jsonify('')
|
self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot()
|
||||||
self.response.status_code = 201
|
|
||||||
return self.response
|
|
||||||
|
|
||||||
snapshot = self.build()
|
snapshot = self.build()
|
||||||
db.session.add(snapshot)
|
db.session.add(snapshot)
|
||||||
db.session().final_flush()
|
db.session().final_flush()
|
||||||
db.session.commit()
|
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
|
self.response.status_code = 201
|
||||||
move_json(self.tmp_snapshots, self.path_snapshot, g.user.email)
|
move_json(self.tmp_snapshots, self.path_snapshot, g.user.email)
|
||||||
return self.response
|
return self.response
|
||||||
|
@ -74,11 +79,15 @@ class InventoryView(LoginMix, SnapshotMix):
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
txt = "{}".format(err)
|
txt = "{}".format(err)
|
||||||
uuid = snapshot_json.get('uuid')
|
uuid = snapshot_json.get('uuid')
|
||||||
|
sid = snapshot_json.get('sid')
|
||||||
error = SnapshotErrors(
|
error = SnapshotErrors(
|
||||||
description=txt, snapshot_uuid=uuid, severity=Severity.Error
|
description=txt, snapshot_uuid=uuid, severity=Severity.Error, sid=sid
|
||||||
)
|
)
|
||||||
error.save(commit=True)
|
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'))
|
api.add_url_rule('/inventory/', view_func=InventoryView.as_view('inventory'))
|
||||||
|
|
|
@ -2,7 +2,10 @@ from inspect import isclass
|
||||||
from typing import Dict, Iterable, Type, Union
|
from typing import Dict, Iterable, Type, Union
|
||||||
|
|
||||||
from ereuse_utils.test import JSON, Res
|
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 werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from ereuse_devicehub.resources import models, schemas
|
from ereuse_devicehub.resources import models, schemas
|
||||||
|
@ -13,110 +16,156 @@ ResourceLike = Union[Type[Union[models.Thing, schemas.Thing]], str]
|
||||||
class Client(TealClient):
|
class Client(TealClient):
|
||||||
"""A client suited for Devicehub main usage."""
|
"""A client suited for Devicehub main usage."""
|
||||||
|
|
||||||
def __init__(self, application,
|
def __init__(
|
||||||
response_wrapper=None,
|
self,
|
||||||
use_cookies=False,
|
application,
|
||||||
allow_subdomain_redirects=False):
|
response_wrapper=None,
|
||||||
super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects)
|
use_cookies=False,
|
||||||
|
allow_subdomain_redirects=False,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
application, response_wrapper, use_cookies, allow_subdomain_redirects
|
||||||
|
)
|
||||||
|
|
||||||
def open(self,
|
def open(
|
||||||
uri: str,
|
self,
|
||||||
res: ResourceLike = None,
|
uri: str,
|
||||||
status: Status = 200,
|
res: ResourceLike = None,
|
||||||
query: Query = tuple(),
|
status: Status = 200,
|
||||||
accept=JSON,
|
query: Query = tuple(),
|
||||||
content_type=JSON,
|
accept=JSON,
|
||||||
item=None,
|
content_type=JSON,
|
||||||
headers: dict = None,
|
item=None,
|
||||||
token: str = None,
|
headers: dict = None,
|
||||||
**kw) -> Res:
|
token: str = None,
|
||||||
|
**kw,
|
||||||
|
) -> Res:
|
||||||
if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)):
|
if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)):
|
||||||
res = res.t
|
res = res.t
|
||||||
return super().open(uri, res, status, query, accept, content_type, item, headers, token,
|
return super().open(
|
||||||
**kw)
|
uri, res, status, query, accept, content_type, item, headers, token, **kw
|
||||||
|
)
|
||||||
|
|
||||||
def get(self,
|
def get(
|
||||||
uri: str = '',
|
self,
|
||||||
res: ResourceLike = None,
|
uri: str = '',
|
||||||
query: Query = tuple(),
|
res: ResourceLike = None,
|
||||||
status: Status = 200,
|
query: Query = tuple(),
|
||||||
item: Union[int, str] = None,
|
status: Status = 200,
|
||||||
accept: str = JSON,
|
item: Union[int, str] = None,
|
||||||
headers: dict = None,
|
accept: str = JSON,
|
||||||
token: str = None,
|
headers: dict = None,
|
||||||
**kw) -> Res:
|
token: str = None,
|
||||||
|
**kw,
|
||||||
|
) -> Res:
|
||||||
return super().get(uri, res, query, status, item, accept, headers, token, **kw)
|
return super().get(uri, res, query, status, item, accept, headers, token, **kw)
|
||||||
|
|
||||||
def post(self,
|
def post(
|
||||||
data: str or dict,
|
self,
|
||||||
uri: str = '',
|
data: str or dict,
|
||||||
res: ResourceLike = None,
|
uri: str = '',
|
||||||
query: Query = tuple(),
|
res: ResourceLike = None,
|
||||||
status: Status = 201,
|
query: Query = tuple(),
|
||||||
content_type: str = JSON,
|
status: Status = 201,
|
||||||
accept: str = JSON,
|
content_type: str = JSON,
|
||||||
headers: dict = None,
|
accept: str = JSON,
|
||||||
token: str = None,
|
headers: dict = None,
|
||||||
**kw) -> Res:
|
token: str = None,
|
||||||
return super().post(data, uri, res, query, status, content_type, accept, headers, token,
|
**kw,
|
||||||
**kw)
|
) -> Res:
|
||||||
|
return super().post(
|
||||||
|
data, uri, res, query, status, content_type, accept, headers, token, **kw
|
||||||
|
)
|
||||||
|
|
||||||
def patch(self,
|
def patch(
|
||||||
data: str or dict,
|
self,
|
||||||
uri: str = '',
|
data: str or dict,
|
||||||
res: ResourceLike = None,
|
uri: str = '',
|
||||||
query: Query = tuple(),
|
res: ResourceLike = None,
|
||||||
item: Union[int, str] = None,
|
query: Query = tuple(),
|
||||||
status: Status = 200,
|
item: Union[int, str] = None,
|
||||||
content_type: str = JSON,
|
status: Status = 200,
|
||||||
accept: str = JSON,
|
content_type: str = JSON,
|
||||||
headers: dict = None,
|
accept: str = JSON,
|
||||||
token: str = None,
|
headers: dict = None,
|
||||||
**kw) -> Res:
|
token: str = None,
|
||||||
return super().patch(data, uri, res, query, item, status, content_type, accept, token,
|
**kw,
|
||||||
headers, **kw)
|
) -> Res:
|
||||||
|
return super().patch(
|
||||||
|
data,
|
||||||
|
uri,
|
||||||
|
res,
|
||||||
|
query,
|
||||||
|
item,
|
||||||
|
status,
|
||||||
|
content_type,
|
||||||
|
accept,
|
||||||
|
token,
|
||||||
|
headers,
|
||||||
|
**kw,
|
||||||
|
)
|
||||||
|
|
||||||
def put(self,
|
def put(
|
||||||
data: str or dict,
|
self,
|
||||||
uri: str = '',
|
data: str or dict,
|
||||||
res: ResourceLike = None,
|
uri: str = '',
|
||||||
query: Query = tuple(),
|
res: ResourceLike = None,
|
||||||
item: Union[int, str] = None,
|
query: Query = tuple(),
|
||||||
status: Status = 201,
|
item: Union[int, str] = None,
|
||||||
content_type: str = JSON,
|
status: Status = 201,
|
||||||
accept: str = JSON,
|
content_type: str = JSON,
|
||||||
headers: dict = None,
|
accept: str = JSON,
|
||||||
token: str = None,
|
headers: dict = None,
|
||||||
**kw) -> Res:
|
token: str = None,
|
||||||
return super().put(data, uri, res, query, item, status, content_type, accept, token,
|
**kw,
|
||||||
headers, **kw)
|
) -> Res:
|
||||||
|
return super().put(
|
||||||
|
data,
|
||||||
|
uri,
|
||||||
|
res,
|
||||||
|
query,
|
||||||
|
item,
|
||||||
|
status,
|
||||||
|
content_type,
|
||||||
|
accept,
|
||||||
|
token,
|
||||||
|
headers,
|
||||||
|
**kw,
|
||||||
|
)
|
||||||
|
|
||||||
def delete(self,
|
def delete(
|
||||||
uri: str = '',
|
self,
|
||||||
res: ResourceLike = None,
|
uri: str = '',
|
||||||
query: Query = tuple(),
|
res: ResourceLike = None,
|
||||||
status: Status = 204,
|
query: Query = tuple(),
|
||||||
item: Union[int, str] = None,
|
status: Status = 204,
|
||||||
accept: str = JSON,
|
item: Union[int, str] = None,
|
||||||
headers: dict = None,
|
accept: str = JSON,
|
||||||
token: str = None,
|
headers: dict = None,
|
||||||
**kw) -> Res:
|
token: str = None,
|
||||||
return super().delete(uri, res, query, status, item, accept, headers, token, **kw)
|
**kw,
|
||||||
|
) -> Res:
|
||||||
|
return super().delete(
|
||||||
|
uri, res, query, status, item, accept, headers, token, **kw
|
||||||
|
)
|
||||||
|
|
||||||
def login(self, email: str, password: str):
|
def login(self, email: str, password: str):
|
||||||
assert isinstance(email, str)
|
assert isinstance(email, str)
|
||||||
assert isinstance(password, 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,
|
def get_many(
|
||||||
res: ResourceLike,
|
self,
|
||||||
resources: Iterable[Union[dict, int]],
|
res: ResourceLike,
|
||||||
key: str = None,
|
resources: Iterable[Union[dict, int]],
|
||||||
**kw) -> Iterable[Union[Dict[str, object], str]]:
|
key: str = None,
|
||||||
|
**kw,
|
||||||
|
) -> Iterable[Union[Dict[str, object], str]]:
|
||||||
"""Like :meth:`.get` but with many resources."""
|
"""Like :meth:`.get` but with many resources."""
|
||||||
return (
|
return (
|
||||||
self.get(res=res, item=r[key] if key else r, **kw)[0]
|
self.get(res=res, item=r[key] if key else r, **kw)[0] for r in resources
|
||||||
for r in resources
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -126,33 +175,119 @@ class UserClient(Client):
|
||||||
It will automatically perform login on the first request.
|
It will automatically perform login on the first request.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, application,
|
def __init__(
|
||||||
email: str,
|
self,
|
||||||
password: str,
|
application,
|
||||||
response_wrapper=None,
|
email: str,
|
||||||
use_cookies=False,
|
password: str,
|
||||||
allow_subdomain_redirects=False):
|
response_wrapper=None,
|
||||||
super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects)
|
use_cookies=False,
|
||||||
|
allow_subdomain_redirects=False,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
application, response_wrapper, use_cookies, allow_subdomain_redirects
|
||||||
|
)
|
||||||
self.email = email # type: str
|
self.email = email # type: str
|
||||||
self.password = password # type: str
|
self.password = password # type: str
|
||||||
self.user = None # type: dict
|
self.user = None # type: dict
|
||||||
|
|
||||||
def open(self,
|
def open(
|
||||||
uri: str,
|
self,
|
||||||
res: ResourceLike = None,
|
uri: str,
|
||||||
status: int or HTTPException = 200,
|
res: ResourceLike = None,
|
||||||
query: Query = tuple(),
|
status: int or HTTPException = 200,
|
||||||
accept=JSON,
|
query: Query = tuple(),
|
||||||
content_type=JSON,
|
accept=JSON,
|
||||||
item=None,
|
content_type=JSON,
|
||||||
headers: dict = None,
|
item=None,
|
||||||
token: str = None,
|
headers: dict = None,
|
||||||
**kw) -> Res:
|
token: str = None,
|
||||||
return super().open(uri, res, status, query, accept, content_type, item, headers,
|
**kw,
|
||||||
self.user['token'] if self.user else token, **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
|
# noinspection PyMethodOverriding
|
||||||
def login(self):
|
def login(self):
|
||||||
response = super().login(self.email, self.password)
|
response = super().login(self.email, self.password)
|
||||||
self.user = response[0]
|
self.user = response[0]
|
||||||
return response
|
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)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
from flask import g
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
from wtforms import BooleanField, EmailField, PasswordField, validators
|
from wtforms import BooleanField, EmailField, PasswordField, validators
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,3 +61,43 @@ class LoginForm(FlaskForm):
|
||||||
self.form_errors.append(self.error_messages['inactive'])
|
self.form_errors.append(self.error_messages['inactive'])
|
||||||
|
|
||||||
return user.is_active
|
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
|
||||||
|
|
|
@ -58,16 +58,30 @@ from ereuse_devicehub.resources.tradedocument.models import TradeDocument
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
DEVICES = {
|
DEVICES = {
|
||||||
"All": ["All"],
|
"All": ["All Devices", "All Components"],
|
||||||
"Computer": [
|
"Computer": [
|
||||||
|
"All Computers",
|
||||||
"Desktop",
|
"Desktop",
|
||||||
"Laptop",
|
"Laptop",
|
||||||
"Server",
|
"Server",
|
||||||
],
|
],
|
||||||
"Monitor": ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"],
|
"Monitor": [
|
||||||
"Mobile, tablet & smartphone": ["Mobile", "Tablet", "Smartphone", "Cellphone"],
|
"All Monitors",
|
||||||
"DataStorage": ["HardDrive", "SolidStateDrive"],
|
"ComputerMonitor",
|
||||||
|
"Monitor",
|
||||||
|
"TelevisionSet",
|
||||||
|
"Projector",
|
||||||
|
],
|
||||||
|
"Mobile, tablet & smartphone": [
|
||||||
|
"All Mobile",
|
||||||
|
"Mobile",
|
||||||
|
"Tablet",
|
||||||
|
"Smartphone",
|
||||||
|
"Cellphone",
|
||||||
|
],
|
||||||
|
"DataStorage": ["All DataStorage", "HardDrive", "SolidStateDrive"],
|
||||||
"Accessories & Peripherals": [
|
"Accessories & Peripherals": [
|
||||||
|
"All Peripherals",
|
||||||
"GraphicCard",
|
"GraphicCard",
|
||||||
"Motherboard",
|
"Motherboard",
|
||||||
"NetworkAdapter",
|
"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):
|
class FilterForm(FlaskForm):
|
||||||
filter = SelectField(
|
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)
|
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]
|
types_of_devices = [item for sublist in DEVICES.values() for item in sublist]
|
||||||
dev = request.args.get('filter')
|
dev = request.args.get('filter')
|
||||||
self.device = dev if dev in types_of_devices else None
|
self.device_type = dev if dev in types_of_devices else None
|
||||||
if self.device:
|
if self.device_type:
|
||||||
self.filter.data = self.device
|
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):
|
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:
|
# Generic Filters
|
||||||
return [self.device]
|
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):
|
class LotForm(FlaskForm):
|
||||||
|
@ -208,12 +300,12 @@ class UploadSnapshotForm(FlaskForm, SnapshotMix):
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
txt = "{}".format(err)
|
txt = "{}".format(err)
|
||||||
uuid = snapshot_json.get('uuid')
|
uuid = snapshot_json.get('uuid')
|
||||||
wbid = snapshot_json.get('wbid')
|
sid = snapshot_json.get('sid')
|
||||||
error = SnapshotErrors(
|
error = SnapshotErrors(
|
||||||
description=txt,
|
description=txt,
|
||||||
snapshot_uuid=uuid,
|
snapshot_uuid=uuid,
|
||||||
severity=Severity.Error,
|
severity=Severity.Error,
|
||||||
wbid=wbid,
|
sid=sid,
|
||||||
)
|
)
|
||||||
error.save(commit=True)
|
error.save(commit=True)
|
||||||
self.result[filename] = 'Error'
|
self.result[filename] = 'Error'
|
||||||
|
@ -468,7 +560,7 @@ class TagDeviceForm(FlaskForm):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
class NewActionForm(FlaskForm):
|
class ActionFormMix(FlaskForm):
|
||||||
name = StringField(
|
name = StringField(
|
||||||
'Name',
|
'Name',
|
||||||
[validators.length(max=50)],
|
[validators.length(max=50)],
|
||||||
|
@ -500,17 +592,23 @@ class NewActionForm(FlaskForm):
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._devices = OrderedSet()
|
if self.type.data in [None, '']:
|
||||||
if self.devices.data:
|
return False
|
||||||
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:
|
if not self.devices.data:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
|
@ -543,29 +641,83 @@ class NewActionForm(FlaskForm):
|
||||||
return self.type.data
|
return self.type.data
|
||||||
|
|
||||||
|
|
||||||
class AllocateForm(NewActionForm):
|
class NewActionForm(ActionFormMix):
|
||||||
start_time = DateField('Start time')
|
|
||||||
end_time = DateField('End time')
|
|
||||||
final_user_code = StringField('Final user code', [validators.length(max=50)])
|
|
||||||
transaction = StringField('Transaction', [validators.length(max=50)])
|
|
||||||
end_users = IntegerField('End users')
|
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self, extra_validators=None):
|
||||||
is_valid = super().validate(extra_validators)
|
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
|
start_time = self.start_time.data
|
||||||
end_time = self.end_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:
|
if start_time and end_time and end_time < start_time:
|
||||||
error = ['The action cannot finish before it starts.']
|
error = ['The action cannot finish before it starts.']
|
||||||
self.start_time.errors = error
|
|
||||||
self.end_time.errors = error
|
self.end_time.errors = error
|
||||||
is_valid = False
|
return False
|
||||||
|
|
||||||
if not self.end_users.data:
|
if not end_time:
|
||||||
self.end_users.errors = ["You need to specify a number of users"]
|
self.end_time.data = self.start_time.data
|
||||||
is_valid = False
|
|
||||||
|
|
||||||
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):
|
class DataWipeDocumentForm(Form):
|
||||||
|
@ -621,7 +773,7 @@ class DataWipeDocumentForm(Form):
|
||||||
return self._obj
|
return self._obj
|
||||||
|
|
||||||
|
|
||||||
class DataWipeForm(NewActionForm):
|
class DataWipeForm(ActionFormMix):
|
||||||
document = FormField(DataWipeDocumentForm)
|
document = FormField(DataWipeDocumentForm)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
@ -648,7 +800,7 @@ class DataWipeForm(NewActionForm):
|
||||||
return self.instance
|
return self.instance
|
||||||
|
|
||||||
|
|
||||||
class TradeForm(NewActionForm):
|
class TradeForm(ActionFormMix):
|
||||||
user_from = StringField(
|
user_from = StringField(
|
||||||
'Supplier',
|
'Supplier',
|
||||||
[validators.Optional()],
|
[validators.Optional()],
|
||||||
|
@ -695,6 +847,9 @@ class TradeForm(NewActionForm):
|
||||||
email_from = self.user_from.data
|
email_from = self.user_from.data
|
||||||
email_to = self.user_to.data
|
email_to = self.user_to.data
|
||||||
|
|
||||||
|
if self.type.data != "Trade":
|
||||||
|
return False
|
||||||
|
|
||||||
if not self.confirm.data and not self.code.data:
|
if not self.confirm.data and not self.code.data:
|
||||||
self.code.errors = ["If you don't want to confirm, you need a code"]
|
self.code.errors = ["If you don't want to confirm, you need a code"]
|
||||||
is_valid = False
|
is_valid = False
|
||||||
|
|
|
@ -7,10 +7,9 @@ import flask_weasyprint
|
||||||
from flask import Blueprint, g, make_response, request, url_for
|
from flask import Blueprint, g, make_response, request, url_for
|
||||||
from flask.views import View
|
from flask.views import View
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from sqlalchemy import or_
|
|
||||||
from werkzeug.exceptions import NotFound
|
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.db import db
|
||||||
from ereuse_devicehub.inventory.forms import (
|
from ereuse_devicehub.inventory.forms import (
|
||||||
AllocateForm,
|
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.hash_reports import insert_hash
|
||||||
from ereuse_devicehub.resources.lot.models import Lot
|
from ereuse_devicehub.resources.lot.models import Lot
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
|
from ereuse_devicehub.views import GenericMixView
|
||||||
|
|
||||||
devices = Blueprint('inventory', __name__, url_prefix='/inventory')
|
devices = Blueprint('inventory', __name__, url_prefix='/inventory')
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class DeviceListMix(GenericMixView):
|
||||||
decorators = [login_required]
|
|
||||||
template_name = 'inventory/device_list.html'
|
template_name = 'inventory/device_list.html'
|
||||||
|
|
||||||
def get_context(self, lot_id):
|
def get_context(self, lot_id):
|
||||||
form_filter = FilterForm()
|
super().get_context()
|
||||||
filter_types = form_filter.search()
|
lots = self.context['lots']
|
||||||
lots = self.get_lots()
|
form_filter = FilterForm(lots, lot_id)
|
||||||
|
devices = form_filter.search()
|
||||||
lot = None
|
lot = None
|
||||||
tags = (
|
tags = (
|
||||||
Tag.query.filter(Tag.owner_id == current_user.id)
|
Tag.query.filter(Tag.owner_id == current_user.id)
|
||||||
|
@ -70,10 +55,6 @@ class DeviceListMix(GenericMixView):
|
||||||
|
|
||||||
if lot_id:
|
if lot_id:
|
||||||
lot = lots.filter(Lot.id == lot_id).one()
|
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_action = NewActionForm(lot=lot.id)
|
||||||
form_new_allocate = AllocateForm(lot=lot.id)
|
form_new_allocate = AllocateForm(lot=lot.id)
|
||||||
form_new_datawipe = DataWipeForm(lot=lot.id)
|
form_new_datawipe = DataWipeForm(lot=lot.id)
|
||||||
|
@ -83,20 +64,6 @@ class DeviceListMix(GenericMixView):
|
||||||
user_from=g.user.email,
|
user_from=g.user.email,
|
||||||
)
|
)
|
||||||
else:
|
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_action = NewActionForm()
|
||||||
form_new_allocate = AllocateForm()
|
form_new_allocate = AllocateForm()
|
||||||
form_new_datawipe = DataWipeForm()
|
form_new_datawipe = DataWipeForm()
|
||||||
|
@ -106,21 +73,21 @@ class DeviceListMix(GenericMixView):
|
||||||
if action_devices:
|
if action_devices:
|
||||||
list_devices.extend([int(x) for x in action_devices.split(",")])
|
list_devices.extend([int(x) for x in action_devices.split(",")])
|
||||||
|
|
||||||
self.context = {
|
self.context.update(
|
||||||
'devices': devices,
|
{
|
||||||
'lots': lots,
|
'devices': devices,
|
||||||
'form_tag_device': TagDeviceForm(),
|
'form_tag_device': TagDeviceForm(),
|
||||||
'form_new_action': form_new_action,
|
'form_new_action': form_new_action,
|
||||||
'form_new_allocate': form_new_allocate,
|
'form_new_allocate': form_new_allocate,
|
||||||
'form_new_datawipe': form_new_datawipe,
|
'form_new_datawipe': form_new_datawipe,
|
||||||
'form_new_trade': form_new_trade,
|
'form_new_trade': form_new_trade,
|
||||||
'form_filter': form_filter,
|
'form_filter': form_filter,
|
||||||
'form_print_labels': PrintLabelsForm(),
|
'form_print_labels': PrintLabelsForm(),
|
||||||
'lot': lot,
|
'lot': lot,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
'list_devices': list_devices,
|
'list_devices': list_devices,
|
||||||
'version': __version__,
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
return self.context
|
return self.context
|
||||||
|
|
||||||
|
@ -136,20 +103,20 @@ class DeviceDetailView(GenericMixView):
|
||||||
template_name = 'inventory/device_detail.html'
|
template_name = 'inventory/device_detail.html'
|
||||||
|
|
||||||
def dispatch_request(self, id):
|
def dispatch_request(self, id):
|
||||||
lots = self.get_lots()
|
self.get_context()
|
||||||
device = (
|
device = (
|
||||||
Device.query.filter(Device.owner_id == current_user.id)
|
Device.query.filter(Device.owner_id == current_user.id)
|
||||||
.filter(Device.devicehub_id == id)
|
.filter(Device.devicehub_id == id)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
|
|
||||||
context = {
|
self.context.update(
|
||||||
'device': device,
|
{
|
||||||
'lots': lots,
|
'device': device,
|
||||||
'page_title': 'Device {}'.format(device.devicehub_id),
|
'page_title': 'Device {}'.format(device.devicehub_id),
|
||||||
'version': __version__,
|
}
|
||||||
}
|
)
|
||||||
return flask.render_template(self.template_name, **context)
|
return flask.render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
class LotCreateView(GenericMixView):
|
class LotCreateView(GenericMixView):
|
||||||
|
@ -165,17 +132,17 @@ class LotCreateView(GenericMixView):
|
||||||
next_url = url_for('inventory.lotdevicelist', lot_id=form.id)
|
next_url = url_for('inventory.lotdevicelist', lot_id=form.id)
|
||||||
return flask.redirect(next_url)
|
return flask.redirect(next_url)
|
||||||
|
|
||||||
lots = self.get_lots()
|
self.get_context()
|
||||||
context = {
|
self.context.update(
|
||||||
'form': form,
|
{
|
||||||
'title': self.title,
|
'form': form,
|
||||||
'lots': lots,
|
'title': self.title,
|
||||||
'version': __version__,
|
}
|
||||||
}
|
)
|
||||||
return flask.render_template(self.template_name, **context)
|
return flask.render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
class LotUpdateView(View):
|
class LotUpdateView(GenericMixView):
|
||||||
methods = ['GET', 'POST']
|
methods = ['GET', 'POST']
|
||||||
decorators = [login_required]
|
decorators = [login_required]
|
||||||
template_name = 'inventory/lot.html'
|
template_name = 'inventory/lot.html'
|
||||||
|
@ -188,14 +155,14 @@ class LotUpdateView(View):
|
||||||
next_url = url_for('inventory.lotdevicelist', lot_id=id)
|
next_url = url_for('inventory.lotdevicelist', lot_id=id)
|
||||||
return flask.redirect(next_url)
|
return flask.redirect(next_url)
|
||||||
|
|
||||||
lots = Lot.query.filter(Lot.owner_id == current_user.id)
|
self.get_context()
|
||||||
context = {
|
self.context.update(
|
||||||
'form': form,
|
{
|
||||||
'title': self.title,
|
'form': form,
|
||||||
'lots': lots,
|
'title': self.title,
|
||||||
'version': __version__,
|
}
|
||||||
}
|
)
|
||||||
return flask.render_template(self.template_name, **context)
|
return flask.render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
class LotDeleteView(View):
|
class LotDeleteView(View):
|
||||||
|
@ -222,25 +189,26 @@ class UploadSnapshotView(GenericMixView):
|
||||||
template_name = 'inventory/upload_snapshot.html'
|
template_name = 'inventory/upload_snapshot.html'
|
||||||
|
|
||||||
def dispatch_request(self, lot_id=None):
|
def dispatch_request(self, lot_id=None):
|
||||||
lots = self.get_lots()
|
self.get_context()
|
||||||
form = UploadSnapshotForm()
|
form = UploadSnapshotForm()
|
||||||
context = {
|
self.context.update(
|
||||||
'page_title': 'Upload Snapshot',
|
{
|
||||||
'lots': lots,
|
'page_title': 'Upload Snapshot',
|
||||||
'form': form,
|
'form': form,
|
||||||
'lot_id': lot_id,
|
'lot_id': lot_id,
|
||||||
'version': __version__,
|
}
|
||||||
}
|
)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
snapshot, devices = form.save(commit=False)
|
snapshot, devices = form.save(commit=False)
|
||||||
if lot_id:
|
if lot_id:
|
||||||
|
lots = self.context['lots']
|
||||||
lot = lots.filter(Lot.id == lot_id).one()
|
lot = lots.filter(Lot.id == lot_id).one()
|
||||||
for dev in devices:
|
for dev in devices:
|
||||||
lot.devices.add(dev)
|
lot.devices.add(dev)
|
||||||
db.session.add(lot)
|
db.session.add(lot)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return flask.render_template(self.template_name, **context)
|
return flask.render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
class DeviceCreateView(GenericMixView):
|
class DeviceCreateView(GenericMixView):
|
||||||
|
@ -249,20 +217,21 @@ class DeviceCreateView(GenericMixView):
|
||||||
template_name = 'inventory/device_create.html'
|
template_name = 'inventory/device_create.html'
|
||||||
|
|
||||||
def dispatch_request(self, lot_id=None):
|
def dispatch_request(self, lot_id=None):
|
||||||
lots = self.get_lots()
|
self.get_context()
|
||||||
form = NewDeviceForm()
|
form = NewDeviceForm()
|
||||||
context = {
|
self.context.update(
|
||||||
'page_title': 'New Device',
|
{
|
||||||
'lots': lots,
|
'page_title': 'New Device',
|
||||||
'form': form,
|
'form': form,
|
||||||
'lot_id': lot_id,
|
'lot_id': lot_id,
|
||||||
'version': __version__,
|
}
|
||||||
}
|
)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
snapshot = form.save(commit=False)
|
snapshot = form.save(commit=False)
|
||||||
next_url = url_for('inventory.devicelist')
|
next_url = url_for('inventory.devicelist')
|
||||||
if lot_id:
|
if lot_id:
|
||||||
next_url = url_for('inventory.lotdevicelist', lot_id=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 = lots.filter(Lot.id == lot_id).one()
|
||||||
lot.devices.add(snapshot.device)
|
lot.devices.add(snapshot.device)
|
||||||
db.session.add(lot)
|
db.session.add(lot)
|
||||||
|
@ -271,7 +240,7 @@ class DeviceCreateView(GenericMixView):
|
||||||
messages.success('Device "{}" created successfully!'.format(form.type.data))
|
messages.success('Device "{}" created successfully!'.format(form.type.data))
|
||||||
return flask.redirect(next_url)
|
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):
|
class TagLinkDeviceView(View):
|
||||||
|
@ -287,13 +256,13 @@ class TagLinkDeviceView(View):
|
||||||
return flask.redirect(request.referrer)
|
return flask.redirect(request.referrer)
|
||||||
|
|
||||||
|
|
||||||
class TagUnlinkDeviceView(View):
|
class TagUnlinkDeviceView(GenericMixView):
|
||||||
methods = ['POST', 'GET']
|
methods = ['POST', 'GET']
|
||||||
decorators = [login_required]
|
decorators = [login_required]
|
||||||
template_name = 'inventory/tag_unlink_device.html'
|
template_name = 'inventory/tag_unlink_device.html'
|
||||||
|
|
||||||
def dispatch_request(self, id):
|
def dispatch_request(self, id):
|
||||||
lots = Lot.query.filter(Lot.owner_id == current_user.id)
|
self.get_context()
|
||||||
form = TagDeviceForm(delete=True, device=id)
|
form = TagDeviceForm(delete=True, device=id)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
form.remove()
|
form.remove()
|
||||||
|
@ -301,14 +270,15 @@ class TagUnlinkDeviceView(View):
|
||||||
next_url = url_for('inventory.devicelist')
|
next_url = url_for('inventory.devicelist')
|
||||||
return flask.redirect(next_url)
|
return flask.redirect(next_url)
|
||||||
|
|
||||||
return flask.render_template(
|
self.context.update(
|
||||||
self.template_name,
|
{
|
||||||
form=form,
|
'form': form,
|
||||||
lots=lots,
|
'referrer': request.referrer,
|
||||||
referrer=request.referrer,
|
}
|
||||||
version=__version__,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return flask.render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
class NewActionView(View):
|
class NewActionView(View):
|
||||||
methods = ['POST']
|
methods = ['POST']
|
||||||
|
@ -317,16 +287,19 @@ class NewActionView(View):
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
self.form = self.form_class()
|
self.form = self.form_class()
|
||||||
|
next_url = self.get_next_url()
|
||||||
|
|
||||||
if self.form.validate_on_submit():
|
if self.form.validate_on_submit():
|
||||||
self.form.save()
|
self.form.save()
|
||||||
messages.success(
|
messages.success(
|
||||||
'Action "{}" created successfully!'.format(self.form.type.data)
|
'Action "{}" created successfully!'.format(self.form.type.data)
|
||||||
)
|
)
|
||||||
|
|
||||||
next_url = self.get_next_url()
|
next_url = self.get_next_url()
|
||||||
return flask.redirect(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):
|
def get_next_url(self):
|
||||||
lot_id = self.form.lot.data
|
lot_id = self.form.lot.data
|
||||||
|
|
||||||
|
@ -352,10 +325,12 @@ class NewAllocateView(NewActionView, DeviceListMix):
|
||||||
next_url = self.get_next_url()
|
next_url = self.get_next_url()
|
||||||
return flask.redirect(next_url)
|
return flask.redirect(next_url)
|
||||||
|
|
||||||
lot_id = self.form.lot.data
|
messages.error('Action {} error!'.format(self.form.type.data))
|
||||||
self.get_context(lot_id)
|
for k, v in self.form.errors.items():
|
||||||
self.context['form_new_allocate'] = self.form
|
value = ';'.join(v)
|
||||||
return flask.render_template(self.template_name, **self.context)
|
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):
|
class NewDataWipeView(NewActionView, DeviceListMix):
|
||||||
|
@ -374,10 +349,9 @@ class NewDataWipeView(NewActionView, DeviceListMix):
|
||||||
next_url = self.get_next_url()
|
next_url = self.get_next_url()
|
||||||
return flask.redirect(next_url)
|
return flask.redirect(next_url)
|
||||||
|
|
||||||
lot_id = self.form.lot.data
|
messages.error('Action {} error!'.format(self.form.type.data))
|
||||||
self.get_context(lot_id)
|
next_url = self.get_next_url()
|
||||||
self.context['form_new_datawipe'] = self.form
|
return flask.redirect(next_url)
|
||||||
return flask.render_template(self.template_name, **self.context)
|
|
||||||
|
|
||||||
|
|
||||||
class NewTradeView(NewActionView, DeviceListMix):
|
class NewTradeView(NewActionView, DeviceListMix):
|
||||||
|
@ -396,10 +370,9 @@ class NewTradeView(NewActionView, DeviceListMix):
|
||||||
next_url = self.get_next_url()
|
next_url = self.get_next_url()
|
||||||
return flask.redirect(next_url)
|
return flask.redirect(next_url)
|
||||||
|
|
||||||
lot_id = self.form.lot.data
|
messages.error('Action {} error!'.format(self.form.type.data))
|
||||||
self.get_context(lot_id)
|
next_url = self.get_next_url()
|
||||||
self.context['form_new_trade'] = self.form
|
return flask.redirect(next_url)
|
||||||
return flask.render_template(self.template_name, **self.context)
|
|
||||||
|
|
||||||
|
|
||||||
class NewTradeDocumentView(View):
|
class NewTradeDocumentView(View):
|
||||||
|
@ -411,6 +384,7 @@ class NewTradeDocumentView(View):
|
||||||
|
|
||||||
def dispatch_request(self, lot_id):
|
def dispatch_request(self, lot_id):
|
||||||
self.form = self.form_class(lot=lot_id)
|
self.form = self.form_class(lot=lot_id)
|
||||||
|
self.get_context()
|
||||||
|
|
||||||
if self.form.validate_on_submit():
|
if self.form.validate_on_submit():
|
||||||
self.form.save()
|
self.form.save()
|
||||||
|
@ -418,9 +392,8 @@ class NewTradeDocumentView(View):
|
||||||
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
|
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
|
||||||
return flask.redirect(next_url)
|
return flask.redirect(next_url)
|
||||||
|
|
||||||
return flask.render_template(
|
self.context.update({'form': self.form, 'title': self.title})
|
||||||
self.template_name, form=self.form, title=self.title, version=__version__
|
return flask.render_template(self.template_name, **self.context)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExportsView(View):
|
class ExportsView(View):
|
||||||
|
@ -432,7 +405,6 @@ class ExportsView(View):
|
||||||
'metrics': self.metrics,
|
'metrics': self.metrics,
|
||||||
'devices': self.devices_list,
|
'devices': self.devices_list,
|
||||||
'certificates': self.erasure,
|
'certificates': self.erasure,
|
||||||
'links': self.public_links,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if export_id not in export_ids:
|
if export_id not in export_ids:
|
||||||
|
@ -508,19 +480,6 @@ class ExportsView(View):
|
||||||
|
|
||||||
return self.response_csv(data, "actions_export.csv")
|
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):
|
def erasure(self):
|
||||||
template = self.build_erasure_certificate()
|
template = self.build_erasure_certificate()
|
||||||
res = flask_weasyprint.render_pdf(
|
res = flask_weasyprint.render_pdf(
|
||||||
|
|
|
@ -64,10 +64,7 @@ class PrintLabelsForm(FlaskForm):
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
# print only tags that are DHID
|
if not self._devices:
|
||||||
dhids = [x.devicehub_id for x in self._devices]
|
return False
|
||||||
self._tags = (
|
|
||||||
Tag.query.filter(Tag.owner_id == g.user.id).filter(Tag.id.in_(dhids)).all()
|
|
||||||
)
|
|
||||||
|
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
|
@ -27,7 +27,7 @@ class TagListView(View):
|
||||||
context = {
|
context = {
|
||||||
'lots': lots,
|
'lots': lots,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
'page_title': 'Tags Management',
|
'page_title': 'Unique Identifiers Management',
|
||||||
'version': __version__,
|
'version': __version__,
|
||||||
}
|
}
|
||||||
return flask.render_template(self.template_name, **context)
|
return flask.render_template(self.template_name, **context)
|
||||||
|
@ -102,7 +102,7 @@ class PrintLabelsView(View):
|
||||||
form = PrintLabelsForm()
|
form = PrintLabelsForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['tags'] = form._tags
|
context['devices'] = form._devices
|
||||||
return flask.render_template(self.template_name, **context)
|
return flask.render_template(self.template_name, **context)
|
||||||
else:
|
else:
|
||||||
messages.error('Error you need select one or more devices')
|
messages.error('Error you need select one or more devices')
|
||||||
|
|
|
@ -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()}')
|
|
@ -1,3 +1,4 @@
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import datetime
|
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 import base2, unit, utils
|
||||||
|
from ereuse_devicehub.parser.models import SnapshotErrors
|
||||||
from ereuse_devicehub.parser.utils import Dumpeable
|
from ereuse_devicehub.parser.utils import Dumpeable
|
||||||
|
from ereuse_devicehub.resources.enums import Severity
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Device(Dumpeable):
|
class Device(Dumpeable):
|
||||||
|
@ -417,7 +422,7 @@ class Computer(Device):
|
||||||
self._ram = None
|
self._ram = None
|
||||||
|
|
||||||
@classmethod
|
@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,
|
Gets hardware information from the computer and its components,
|
||||||
like serial numbers or model names, and benchmarks them.
|
like serial numbers or model names, and benchmarks them.
|
||||||
|
@ -428,17 +433,35 @@ class Computer(Device):
|
||||||
hwinfo = hwinfo_raw.splitlines()
|
hwinfo = hwinfo_raw.splitlines()
|
||||||
computer = cls(lshw)
|
computer = cls(lshw)
|
||||||
components = []
|
components = []
|
||||||
for Component in cls.COMPONENTS:
|
try:
|
||||||
if Component == Display and computer.type != 'Laptop':
|
for Component in cls.COMPONENTS:
|
||||||
continue # Only get display info when computer is laptop
|
if Component == Display and computer.type != 'Laptop':
|
||||||
components.extend(Component.new(lshw=lshw, hwinfo=hwinfo))
|
continue # Only get display info when computer is laptop
|
||||||
components.append(Motherboard.new(lshw, hwinfo))
|
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
|
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:
|
def __str__(self) -> str:
|
||||||
specs = super().__str__()
|
specs = super().__str__()
|
||||||
return '{} with {} MB of RAM.'.format(specs, self._ram)
|
return '{} with {} MB of RAM.'.format(specs, self._ram)
|
||||||
|
|
|
@ -14,7 +14,7 @@ class SnapshotErrors(Thing):
|
||||||
|
|
||||||
id = Column(BigInteger, Sequence('snapshot_errors_seq'), primary_key=True)
|
id = Column(BigInteger, Sequence('snapshot_errors_seq'), primary_key=True)
|
||||||
description = Column(CIText(), default='', nullable=False)
|
description = Column(CIText(), default='', nullable=False)
|
||||||
wbid = Column(CIText(), nullable=True)
|
sid = Column(CIText(), nullable=True)
|
||||||
severity = Column(SmallInteger, default=Severity.Info, nullable=False)
|
severity = Column(SmallInteger, default=Severity.Info, nullable=False)
|
||||||
snapshot_uuid = Column(UUID(as_uuid=True), nullable=False)
|
snapshot_uuid = Column(UUID(as_uuid=True), nullable=False)
|
||||||
owner_id = db.Column(
|
owner_id = db.Column(
|
||||||
|
|
|
@ -3,7 +3,8 @@ import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from dmidecode import DMIParse
|
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 import base2
|
||||||
from ereuse_devicehub.parser.computer import Computer
|
from ereuse_devicehub.parser.computer import Computer
|
||||||
|
@ -38,7 +39,7 @@ class ParseSnapshot:
|
||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
"endTime": snapshot["timestamp"],
|
"endTime": snapshot["timestamp"],
|
||||||
"elapsed": 1,
|
"elapsed": 1,
|
||||||
"wbid": snapshot["wbid"],
|
"sid": snapshot["sid"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_snapshot(self):
|
def get_snapshot(self):
|
||||||
|
@ -120,7 +121,11 @@ class ParseSnapshot:
|
||||||
|
|
||||||
def get_usb_num(self):
|
def get_usb_num(self):
|
||||||
return len(
|
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):
|
def get_serial_num(self):
|
||||||
|
@ -128,7 +133,7 @@ class ParseSnapshot:
|
||||||
[
|
[
|
||||||
u
|
u
|
||||||
for u in self.dmi.get("Port Connector")
|
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
|
u
|
||||||
for u in self.dmi.get("Port Connector")
|
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"):
|
def __init__(self, snapshot, default="n/a"):
|
||||||
self.default = default
|
self.default = default
|
||||||
self.uuid = snapshot.get("uuid")
|
self.uuid = snapshot.get("uuid")
|
||||||
self.wbid = snapshot.get("wbid")
|
self.sid = snapshot.get("sid")
|
||||||
self.dmidecode_raw = snapshot["data"]["dmidecode"]
|
self.dmidecode_raw = snapshot["data"]["dmidecode"]
|
||||||
self.smart = snapshot["data"]["smart"]
|
self.smart = snapshot["data"]["smart"]
|
||||||
self.hwinfo_raw = snapshot["data"]["hwinfo"]
|
self.hwinfo_raw = snapshot["data"]["hwinfo"]
|
||||||
|
@ -339,21 +344,11 @@ class ParseSnapshotLsHw:
|
||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
"endTime": snapshot["timestamp"],
|
"endTime": snapshot["timestamp"],
|
||||||
"elapsed": 1,
|
"elapsed": 1,
|
||||||
"wbid": snapshot["wbid"],
|
"sid": snapshot["sid"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_snapshot(self):
|
def get_snapshot(self):
|
||||||
try:
|
return Snapshot().load(self.snapshot_json)
|
||||||
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
|
|
||||||
|
|
||||||
def parse_hwinfo(self):
|
def parse_hwinfo(self):
|
||||||
hw_blocks = self.hwinfo_raw.split("\n\n")
|
hw_blocks = self.hwinfo_raw.split("\n\n")
|
||||||
|
@ -365,8 +360,24 @@ class ParseSnapshotLsHw:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def set_basic_datas(self):
|
def set_basic_datas(self):
|
||||||
pc, self.components_obj = Computer.run(self.lshw, self.hwinfo_raw)
|
try:
|
||||||
self.device = pc.dump()
|
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()
|
self.device['uuid'] = self.get_uuid()
|
||||||
|
|
||||||
def set_components(self):
|
def set_components(self):
|
||||||
|
@ -405,8 +416,10 @@ class ParseSnapshotLsHw:
|
||||||
def get_ram_size(self, ram):
|
def get_ram_size(self, ram):
|
||||||
size = ram.get("Size")
|
size = ram.get("Size")
|
||||||
if not len(size.split(" ")) == 2:
|
if not len(size.split(" ")) == 2:
|
||||||
txt = "Error: Snapshot: {uuid}, tag: {wbid} have this ram Size: {size}".format(
|
txt = (
|
||||||
uuid=self.uuid, size=size, wbid=self.wbid
|
"Error: Snapshot: {uuid}, tag: {sid} have this ram Size: {size}".format(
|
||||||
|
uuid=self.uuid, size=size, sid=self.sid
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.errors(txt)
|
self.errors(txt)
|
||||||
return 128
|
return 128
|
||||||
|
@ -416,8 +429,8 @@ class ParseSnapshotLsHw:
|
||||||
def get_ram_speed(self, ram):
|
def get_ram_speed(self, ram):
|
||||||
speed = ram.get("Speed", "100")
|
speed = ram.get("Speed", "100")
|
||||||
if not len(speed.split(" ")) == 2:
|
if not len(speed.split(" ")) == 2:
|
||||||
txt = "Error: Snapshot: {uuid}, tag: {wbid} have this ram Speed: {speed}".format(
|
txt = "Error: Snapshot: {uuid}, tag: {sid} have this ram Speed: {speed}".format(
|
||||||
uuid=self.uuid, speed=speed, wbid=self.wbid
|
uuid=self.uuid, speed=speed, sid=self.sid
|
||||||
)
|
)
|
||||||
self.errors(txt)
|
self.errors(txt)
|
||||||
return 100
|
return 100
|
||||||
|
@ -451,8 +464,8 @@ class ParseSnapshotLsHw:
|
||||||
uuid.UUID(dmi_uuid)
|
uuid.UUID(dmi_uuid)
|
||||||
except (ValueError, AttributeError) as err:
|
except (ValueError, AttributeError) as err:
|
||||||
self.errors("{}".format(err))
|
self.errors("{}".format(err))
|
||||||
txt = "Error: Snapshot: {uuid} tag: {wbid} have this uuid: {device}".format(
|
txt = "Error: Snapshot: {uuid} tag: {sid} have this uuid: {device}".format(
|
||||||
uuid=self.uuid, device=dmi_uuid, wbid=self.wbid
|
uuid=self.uuid, device=dmi_uuid, sid=self.sid
|
||||||
)
|
)
|
||||||
self.errors(txt)
|
self.errors(txt)
|
||||||
dmi_uuid = None
|
dmi_uuid = None
|
||||||
|
@ -461,6 +474,8 @@ class ParseSnapshotLsHw:
|
||||||
def get_data_storage(self):
|
def get_data_storage(self):
|
||||||
|
|
||||||
for sm in self.smart:
|
for sm in self.smart:
|
||||||
|
if sm.get('smartctl', {}).get('exit_status') == 1:
|
||||||
|
continue
|
||||||
model = sm.get('model_name')
|
model = sm.get('model_name')
|
||||||
manufacturer = None
|
manufacturer = None
|
||||||
if model and len(model.split(" ")) > 1:
|
if model and len(model.split(" ")) > 1:
|
||||||
|
@ -487,7 +502,7 @@ class ParseSnapshotLsHw:
|
||||||
SSD = 'SolidStateDrive'
|
SSD = 'SolidStateDrive'
|
||||||
HDD = 'HardDrive'
|
HDD = 'HardDrive'
|
||||||
type_dev = x.get('device', {}).get('type')
|
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
|
return SSD if type_dev in SSDS or trim else HDD
|
||||||
|
|
||||||
def get_data_storage_interface(self, x):
|
def get_data_storage_interface(self, x):
|
||||||
|
@ -496,22 +511,21 @@ class ParseSnapshotLsHw:
|
||||||
DataStorageInterface(interface.upper())
|
DataStorageInterface(interface.upper())
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
txt = "tag: {}, interface {} is not in DataStorageInterface Enum".format(
|
txt = "tag: {}, interface {} is not in DataStorageInterface Enum".format(
|
||||||
interface, self.wbid
|
interface, self.sid
|
||||||
)
|
)
|
||||||
self.errors("{}".format(err))
|
self.errors("{}".format(err))
|
||||||
self.errors(txt)
|
self.errors(txt)
|
||||||
return "ATA"
|
return "ATA"
|
||||||
|
|
||||||
def get_data_storage_size(self, x):
|
def get_data_storage_size(self, x):
|
||||||
type_dev = x.get('device', {}).get('protocol', '').lower()
|
total_capacity = x.get('user_capacity', {}).get('bytes')
|
||||||
total_capacity = "{type}_total_capacity".format(type=type_dev)
|
if not total_capacity:
|
||||||
if not x.get(total_capacity):
|
|
||||||
return 1
|
return 1
|
||||||
# convert bytes to Mb
|
# convert bytes to Mb
|
||||||
return x.get(total_capacity) / 1024**2
|
return total_capacity / 1024**2
|
||||||
|
|
||||||
def get_test_data_storage(self, smart):
|
def get_test_data_storage(self, smart):
|
||||||
log = "smart_health_information_log"
|
hours = smart.get("power_on_time", {}).get('hours', 0)
|
||||||
action = {
|
action = {
|
||||||
"status": "Completed without error",
|
"status": "Completed without error",
|
||||||
"reallocatedSectorCount": smart.get("reallocated_sector_count", 0),
|
"reallocatedSectorCount": smart.get("reallocated_sector_count", 0),
|
||||||
|
@ -519,7 +533,8 @@ class ParseSnapshotLsHw:
|
||||||
"assessment": True,
|
"assessment": True,
|
||||||
"severity": "Info",
|
"severity": "Info",
|
||||||
"offlineUncorrectable": smart.get("offline_uncorrectable", 0),
|
"offlineUncorrectable": smart.get("offline_uncorrectable", 0),
|
||||||
"lifetime": 0,
|
"lifetime": hours,
|
||||||
|
"powerOnHours": hours,
|
||||||
"type": "TestDataStorage",
|
"type": "TestDataStorage",
|
||||||
"length": "Short",
|
"length": "Short",
|
||||||
"elapsed": 0,
|
"elapsed": 0,
|
||||||
|
@ -529,11 +544,6 @@ class ParseSnapshotLsHw:
|
||||||
"powerCycleCount": smart.get("power_cycle_count", 0),
|
"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
|
return action
|
||||||
|
|
||||||
def errors(self, txt=None, severity=Severity.Info):
|
def errors(self, txt=None, severity=Severity.Info):
|
||||||
|
@ -543,6 +553,6 @@ class ParseSnapshotLsHw:
|
||||||
logger.error(txt)
|
logger.error(txt)
|
||||||
self._errors.append(txt)
|
self._errors.append(txt)
|
||||||
error = SnapshotErrors(
|
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()
|
error.save()
|
||||||
|
|
|
@ -7,11 +7,11 @@ from ereuse_devicehub.resources.schemas import Thing
|
||||||
|
|
||||||
|
|
||||||
class Snapshot_lite_data(MarshmallowSchema):
|
class Snapshot_lite_data(MarshmallowSchema):
|
||||||
dmidecode = String(required=False)
|
dmidecode = String(required=True)
|
||||||
hwinfo = String(required=False)
|
hwinfo = String(required=True)
|
||||||
smart = List(Dict(), required=False)
|
smart = List(Dict(), required=True)
|
||||||
lshw = Dict(required=False)
|
lshw = Dict(required=True)
|
||||||
lspci = String(required=False)
|
lspci = String(required=True)
|
||||||
|
|
||||||
|
|
||||||
class Snapshot_lite(Thing):
|
class Snapshot_lite(Thing):
|
||||||
|
@ -19,10 +19,10 @@ class Snapshot_lite(Thing):
|
||||||
version = String(required=True)
|
version = String(required=True)
|
||||||
schema_api = String(required=True)
|
schema_api = String(required=True)
|
||||||
software = String(required=True)
|
software = String(required=True)
|
||||||
wbid = String(required=True)
|
sid = String(required=True)
|
||||||
type = String(required=True)
|
type = String(required=True)
|
||||||
timestamp = String(required=True)
|
timestamp = String(required=True)
|
||||||
data = Nested(Snapshot_lite_data)
|
data = Nested(Snapshot_lite_data, required=True)
|
||||||
|
|
||||||
@validates_schema
|
@validates_schema
|
||||||
def validate_workbench_version(self, data: dict):
|
def validate_workbench_version(self, data: dict):
|
||||||
|
|
|
@ -17,12 +17,12 @@ from datetime import datetime, timedelta, timezone
|
||||||
from decimal import ROUND_HALF_EVEN, ROUND_UP, Decimal
|
from decimal import ROUND_HALF_EVEN, ROUND_UP, Decimal
|
||||||
from typing import Optional, Set, Union
|
from typing import Optional, Set, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from dateutil.tz import tzutc
|
|
||||||
|
|
||||||
import inflection
|
import inflection
|
||||||
import teal.db
|
import teal.db
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
|
from dateutil.tz import tzutc
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask import g
|
from flask import g
|
||||||
from sortedcontainers import SortedSet
|
from sortedcontainers import SortedSet
|
||||||
|
@ -274,7 +274,9 @@ class Action(Thing):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def __lt__(self, other):
|
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:
|
def __str__(self) -> str:
|
||||||
return '{}'.format(self.severity)
|
return '{}'.format(self.severity)
|
||||||
|
@ -664,7 +666,7 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
|
||||||
elapsed.comment = """For Snapshots made with Workbench, the total amount
|
elapsed.comment = """For Snapshots made with Workbench, the total amount
|
||||||
of time it took to complete.
|
of time it took to complete.
|
||||||
"""
|
"""
|
||||||
wbid = Column(CIText(), nullable=True)
|
sid = Column(CIText(), nullable=True)
|
||||||
|
|
||||||
def get_last_lifetimes(self):
|
def get_last_lifetimes(self):
|
||||||
"""We get the lifetime and serial_number of the first disk"""
|
"""We get the lifetime and serial_number of the first disk"""
|
||||||
|
|
|
@ -425,7 +425,7 @@ class Snapshot(ActionWithOneDevice):
|
||||||
See docs for more info.
|
See docs for more info.
|
||||||
"""
|
"""
|
||||||
uuid = UUID()
|
uuid = UUID()
|
||||||
wbid = String(required=False)
|
sid = String(required=False)
|
||||||
software = EnumField(
|
software = EnumField(
|
||||||
SnapshotSoftware,
|
SnapshotSoftware,
|
||||||
required=True,
|
required=True,
|
||||||
|
|
|
@ -3,7 +3,9 @@ from operator import attrgetter
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from citext import CIText
|
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.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.orm import backref, relationship, validates
|
from sqlalchemy.orm import backref, relationship, validates
|
||||||
|
@ -42,7 +44,7 @@ class Agent(Thing):
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
||||||
UniqueConstraint(tax_id, name, name='One tax ID with one name.'),
|
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
|
@declared_attr
|
||||||
|
@ -63,7 +65,9 @@ class Agent(Thing):
|
||||||
@property
|
@property
|
||||||
def actions(self) -> list:
|
def actions(self) -> list:
|
||||||
# todo test
|
# 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')
|
@validates('name')
|
||||||
def does_not_contain_slash(self, _, value: str):
|
def does_not_contain_slash(self, _, value: str):
|
||||||
|
@ -76,15 +80,17 @@ class Agent(Thing):
|
||||||
|
|
||||||
|
|
||||||
class Organization(JoinedTableMixin, Agent):
|
class Organization(JoinedTableMixin, Agent):
|
||||||
default_of = db.relationship(Inventory,
|
default_of = db.relationship(
|
||||||
uselist=False,
|
Inventory,
|
||||||
lazy=True,
|
uselist=False,
|
||||||
backref=backref('org', lazy=True),
|
lazy=True,
|
||||||
# We need to use this as we cannot do Inventory.foreign -> Org
|
backref=backref('org', lazy=True),
|
||||||
# as foreign keys can only reference to one table
|
# We need to use this as we cannot do Inventory.foreign -> Org
|
||||||
# and we have multiple organization table (one per schema)
|
# as foreign keys can only reference to one table
|
||||||
foreign_keys=[Inventory.org_id],
|
# and we have multiple organization table (one per schema)
|
||||||
primaryjoin=lambda: Organization.id == Inventory.org_id)
|
foreign_keys=[Inventory.org_id],
|
||||||
|
primaryjoin=lambda: Organization.id == Inventory.org_id,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, name: str, **kwargs) -> None:
|
def __init__(self, name: str, **kwargs) -> None:
|
||||||
super().__init__(**kwargs, name=name)
|
super().__init__(**kwargs, name=name)
|
||||||
|
@ -97,12 +103,17 @@ class Organization(JoinedTableMixin, Agent):
|
||||||
|
|
||||||
class Individual(JoinedTableMixin, Agent):
|
class Individual(JoinedTableMixin, Agent):
|
||||||
active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id))
|
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_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True)
|
||||||
user = relationship(User,
|
user = relationship(
|
||||||
backref=backref('individuals', lazy=True, collection_class=set),
|
User,
|
||||||
primaryjoin=user_id == User.id)
|
backref=backref('individuals', lazy=True, collection_class=set),
|
||||||
|
primaryjoin=user_id == User.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Membership(Thing):
|
class Membership(Thing):
|
||||||
|
@ -110,20 +121,29 @@ class Membership(Thing):
|
||||||
|
|
||||||
For example, because the individual works in or because is a member of.
|
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:
|
id = Column(Unicode(), check_lower('id'))
|
||||||
super().__init__(organization=organization,
|
organization_id = Column(
|
||||||
individual=individual,
|
UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True
|
||||||
id=id)
|
)
|
||||||
|
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__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(id, organization_id, name='One member id per organization.'),
|
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 person in the system. There can be several persons pointing to
|
||||||
a real.
|
a real.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
import pathlib
|
|
||||||
import copy
|
import copy
|
||||||
|
import pathlib
|
||||||
import time
|
import time
|
||||||
from flask import g
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import Dict, List, Set
|
from typing import Dict, List, Set
|
||||||
from flask_sqlalchemy import event
|
|
||||||
|
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
|
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 more_itertools import unique_everseen
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
from sqlalchemy import BigInteger, Boolean, Column
|
||||||
Sequence, SmallInteger, Unicode, inspect, text
|
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.dialects.postgresql import UUID
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
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.util import OrderedSet
|
||||||
from sqlalchemy_utils import ColorType
|
from sqlalchemy_utils import ColorType
|
||||||
from stdnum import imei, meid
|
from stdnum import imei, meid
|
||||||
from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, \
|
from teal.db import (
|
||||||
check_lower, check_range, IntEnum
|
CASCADE_DEL,
|
||||||
|
POLYMORPHIC_ID,
|
||||||
|
POLYMORPHIC_ON,
|
||||||
|
URL,
|
||||||
|
IntEnum,
|
||||||
|
ResourceNotFound,
|
||||||
|
check_lower,
|
||||||
|
check_range,
|
||||||
|
)
|
||||||
from teal.enums import Layouts
|
from teal.enums import Layouts
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from teal.resource import url_for_resource
|
from teal.resource import url_for_resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
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.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):
|
def create_code(context):
|
||||||
|
@ -58,17 +90,21 @@ class Device(Thing):
|
||||||
Devices can contain ``Components``, which are just a type of device
|
Devices can contain ``Components``, which are just a type of device
|
||||||
(it is a recursive relationship).
|
(it is a recursive relationship).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
|
||||||
id.comment = """The identifier of the device for this database. Used only
|
id.comment = """The identifier of the device for this database. Used only
|
||||||
internally for software; users should not use this.
|
internally for software; users should not use this.
|
||||||
"""
|
"""
|
||||||
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||||
hid = Column(Unicode(), check_lower('hid'), unique=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
|
systems use to ID a device globally. This field is auto-generated
|
||||||
from Devicehub using literal identifiers from the device,
|
from Devicehub using literal identifiers from the device,
|
||||||
so it can re-generated *offline*.
|
so it can re-generated *offline*.
|
||||||
""" + HID_CONVERSION_DOC
|
"""
|
||||||
|
+ HID_CONVERSION_DOC
|
||||||
|
)
|
||||||
model = Column(Unicode(), check_lower('model'))
|
model = Column(Unicode(), check_lower('model'))
|
||||||
model.comment = """The model of the device in lower case.
|
model.comment = """The model of the device in lower case.
|
||||||
|
|
||||||
|
@ -118,14 +154,18 @@ class Device(Thing):
|
||||||
image = db.Column(db.URL)
|
image = db.Column(db.URL)
|
||||||
image.comment = "An image of the device."
|
image.comment = "An image of the device."
|
||||||
|
|
||||||
owner_id = db.Column(UUID(as_uuid=True),
|
owner_id = db.Column(
|
||||||
db.ForeignKey(User.id),
|
UUID(as_uuid=True),
|
||||||
nullable=False,
|
db.ForeignKey(User.id),
|
||||||
default=lambda: g.user.id)
|
nullable=False,
|
||||||
|
default=lambda: g.user.id,
|
||||||
|
)
|
||||||
owner = db.relationship(User, primaryjoin=owner_id == User.id)
|
owner = db.relationship(User, primaryjoin=owner_id == User.id)
|
||||||
allocated = db.Column(Boolean, default=False)
|
allocated = db.Column(Boolean, default=False)
|
||||||
allocated.comment = "device is allocated or not."
|
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."
|
devicehub_id.comment = "device have a unique code."
|
||||||
active = db.Column(Boolean, default=True)
|
active = db.Column(Boolean, default=True)
|
||||||
|
|
||||||
|
@ -152,12 +192,12 @@ class Device(Thing):
|
||||||
'image',
|
'image',
|
||||||
'allocated',
|
'allocated',
|
||||||
'devicehub_id',
|
'devicehub_id',
|
||||||
'active'
|
'active',
|
||||||
}
|
}
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.Index('device_id', id, postgresql_using='hash'),
|
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:
|
def __init__(self, **kw) -> None:
|
||||||
|
@ -187,7 +227,9 @@ class Device(Thing):
|
||||||
for ac in actions_one:
|
for ac in actions_one:
|
||||||
ac.real_created = ac.created
|
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
|
@property
|
||||||
def problems(self):
|
def problems(self):
|
||||||
|
@ -196,8 +238,9 @@ class Device(Thing):
|
||||||
There can be up to 3 actions: current Snapshot,
|
There can be up to 3 actions: current Snapshot,
|
||||||
current Physical action, current Trading action.
|
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.action.models import Snapshot
|
||||||
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
actions = set()
|
actions = set()
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
actions.add(self.last_action_of(Snapshot))
|
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 ensure to remove materialized values when start using them
|
||||||
# todo or self.__table__.columns if inspect fails
|
# todo or self.__table__.columns if inspect fails
|
||||||
return {c.key: getattr(self, c.key, None)
|
return {
|
||||||
for c in inspect(self.__class__).attrs
|
c.key: getattr(self, c.key, None)
|
||||||
if isinstance(c, ColumnProperty)
|
for c in inspect(self.__class__).attrs
|
||||||
and not getattr(c, 'foreign_keys', None)
|
if isinstance(c, ColumnProperty)
|
||||||
and c.key not in self._NON_PHYSICAL_PROPS}
|
and not getattr(c, 'foreign_keys', None)
|
||||||
|
and c.key not in self._NON_PHYSICAL_PROPS
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_properties(self) -> Dict[str, object or None]:
|
def public_properties(self) -> Dict[str, object or None]:
|
||||||
|
@ -234,11 +279,13 @@ class Device(Thing):
|
||||||
"""
|
"""
|
||||||
non_public = ['amount', 'transfer_state', 'receiver_id']
|
non_public = ['amount', 'transfer_state', 'receiver_id']
|
||||||
hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public
|
hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public
|
||||||
return {c.key: getattr(self, c.key, None)
|
return {
|
||||||
for c in inspect(self.__class__).attrs
|
c.key: getattr(self, c.key, None)
|
||||||
if isinstance(c, ColumnProperty)
|
for c in inspect(self.__class__).attrs
|
||||||
and not getattr(c, 'foreign_keys', None)
|
if isinstance(c, ColumnProperty)
|
||||||
and c.key not in hide_properties}
|
and not getattr(c, 'foreign_keys', None)
|
||||||
|
and c.key not in hide_properties
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_actions(self) -> List[object]:
|
def public_actions(self) -> List[object]:
|
||||||
|
@ -250,6 +297,11 @@ class Device(Thing):
|
||||||
actions.reverse()
|
actions.reverse()
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_link(self) -> str:
|
||||||
|
host_url = request.host_url.strip('/')
|
||||||
|
return "{}{}".format(host_url, self.url.to_text())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
"""The URL where to GET this device."""
|
"""The URL where to GET this device."""
|
||||||
|
@ -260,6 +312,7 @@ class Device(Thing):
|
||||||
"""The last Rate of the device."""
|
"""The last Rate of the device."""
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
from ereuse_devicehub.resources.action.models import Rate
|
from ereuse_devicehub.resources.action.models import Rate
|
||||||
|
|
||||||
return self.last_action_of(Rate)
|
return self.last_action_of(Rate)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -268,12 +321,14 @@ class Device(Thing):
|
||||||
ever been set."""
|
ever been set."""
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
from ereuse_devicehub.resources.action.models import Price
|
from ereuse_devicehub.resources.action.models import Price
|
||||||
|
|
||||||
return self.last_action_of(Price)
|
return self.last_action_of(Price)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_action_trading(self):
|
def last_action_trading(self):
|
||||||
"""which is the last action trading"""
|
"""which is the last action trading"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
return self.last_action_of(*states.Trading.actions())
|
return self.last_action_of(*states.Trading.actions())
|
||||||
|
|
||||||
|
@ -287,6 +342,7 @@ class Device(Thing):
|
||||||
- Management
|
- Management
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
return self.last_action_of(*states.Status.actions())
|
return self.last_action_of(*states.Status.actions())
|
||||||
|
|
||||||
|
@ -300,6 +356,7 @@ class Device(Thing):
|
||||||
- Management
|
- Management
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
status_actions = [ac.t for ac in states.Status.actions()]
|
status_actions = [ac.t for ac in states.Status.actions()]
|
||||||
history = []
|
history = []
|
||||||
for ac in self.actions:
|
for ac in self.actions:
|
||||||
|
@ -329,13 +386,15 @@ class Device(Thing):
|
||||||
if not hasattr(lot, 'trade'):
|
if not hasattr(lot, 'trade'):
|
||||||
return
|
return
|
||||||
|
|
||||||
Status = {0: 'Trade',
|
Status = {
|
||||||
1: 'Confirm',
|
0: 'Trade',
|
||||||
2: 'NeedConfirmation',
|
1: 'Confirm',
|
||||||
3: 'TradeConfirmed',
|
2: 'NeedConfirmation',
|
||||||
4: 'Revoke',
|
3: 'TradeConfirmed',
|
||||||
5: 'NeedConfirmRevoke',
|
4: 'Revoke',
|
||||||
6: 'RevokeConfirmed'}
|
5: 'NeedConfirmRevoke',
|
||||||
|
6: 'RevokeConfirmed',
|
||||||
|
}
|
||||||
|
|
||||||
trade = lot.trade
|
trade = lot.trade
|
||||||
user_from = trade.user_from
|
user_from = trade.user_from
|
||||||
|
@ -408,6 +467,7 @@ class Device(Thing):
|
||||||
"""If the actual trading state is an revoke action, this property show
|
"""If the actual trading state is an revoke action, this property show
|
||||||
the id of that revoke"""
|
the id of that revoke"""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Trading.actions())
|
action = self.last_action_of(*states.Trading.actions())
|
||||||
if action.type == 'Revoke':
|
if action.type == 'Revoke':
|
||||||
|
@ -417,6 +477,7 @@ class Device(Thing):
|
||||||
def physical(self):
|
def physical(self):
|
||||||
"""The actual physical state, None otherwise."""
|
"""The actual physical state, None otherwise."""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Physical.actions())
|
action = self.last_action_of(*states.Physical.actions())
|
||||||
return states.Physical(action.__class__)
|
return states.Physical(action.__class__)
|
||||||
|
@ -425,6 +486,7 @@ class Device(Thing):
|
||||||
def traking(self):
|
def traking(self):
|
||||||
"""The actual traking state, None otherwise."""
|
"""The actual traking state, None otherwise."""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Traking.actions())
|
action = self.last_action_of(*states.Traking.actions())
|
||||||
return states.Traking(action.__class__)
|
return states.Traking(action.__class__)
|
||||||
|
@ -433,6 +495,7 @@ class Device(Thing):
|
||||||
def usage(self):
|
def usage(self):
|
||||||
"""The actual usage state, None otherwise."""
|
"""The actual usage state, None otherwise."""
|
||||||
from ereuse_devicehub.resources.device import states
|
from ereuse_devicehub.resources.device import states
|
||||||
|
|
||||||
with suppress(LookupError, ValueError):
|
with suppress(LookupError, ValueError):
|
||||||
action = self.last_action_of(*states.Usage.actions())
|
action = self.last_action_of(*states.Usage.actions())
|
||||||
return states.Usage(action.__class__)
|
return states.Usage(action.__class__)
|
||||||
|
@ -470,8 +533,11 @@ class Device(Thing):
|
||||||
test has been executed.
|
test has been executed.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.action.models import Test
|
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)
|
return self._warning_actions(current_tests)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -496,7 +562,9 @@ class Device(Thing):
|
||||||
|
|
||||||
def set_hid(self):
|
def set_hid(self):
|
||||||
with suppress(TypeError):
|
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):
|
def last_action_of(self, *types):
|
||||||
"""Gets the last action of the given types.
|
"""Gets the last action of the given types.
|
||||||
|
@ -509,7 +577,9 @@ class Device(Thing):
|
||||||
actions.sort(key=lambda x: x.created)
|
actions.sort(key=lambda x: x.created)
|
||||||
return next(e for e in reversed(actions) if isinstance(e, types))
|
return next(e for e in reversed(actions) if isinstance(e, types))
|
||||||
except StopIteration:
|
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):
|
def which_user_put_this_device_in_trace(self):
|
||||||
"""which is the user than put this device in this trade"""
|
"""which is the user than put this device in this trade"""
|
||||||
|
@ -546,6 +616,32 @@ class Device(Thing):
|
||||||
metrics = Metrics(device=self)
|
metrics = Metrics(device=self)
|
||||||
return metrics.get_metrics()
|
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):
|
def __lt__(self, other):
|
||||||
return self.id < other.id
|
return self.id < other.id
|
||||||
|
|
||||||
|
@ -571,19 +667,24 @@ class Device(Thing):
|
||||||
|
|
||||||
class DisplayMixin:
|
class DisplayMixin:
|
||||||
"""Base class for the Display Component and the Monitor Device."""
|
"""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."""
|
size.comment = """The size of the monitor in inches."""
|
||||||
technology = Column(DBEnum(DisplayTech))
|
technology = Column(DBEnum(DisplayTech))
|
||||||
technology.comment = """The technology the monitor uses to display
|
technology.comment = """The technology the monitor uses to display
|
||||||
the image.
|
the image.
|
||||||
"""
|
"""
|
||||||
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000),
|
resolution_width = Column(
|
||||||
nullable=True)
|
SmallInteger, check_range('resolution_width', 10, 20000), nullable=True
|
||||||
|
)
|
||||||
resolution_width.comment = """The maximum horizontal resolution the
|
resolution_width.comment = """The maximum horizontal resolution the
|
||||||
monitor can natively support in pixels.
|
monitor can natively support in pixels.
|
||||||
"""
|
"""
|
||||||
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000),
|
resolution_height = Column(
|
||||||
nullable=True)
|
SmallInteger, check_range('resolution_height', 10, 20000), nullable=True
|
||||||
|
)
|
||||||
resolution_height.comment = """The maximum vertical resolution the
|
resolution_height.comment = """The maximum vertical resolution the
|
||||||
monitor can natively support in pixels.
|
monitor can natively support in pixels.
|
||||||
"""
|
"""
|
||||||
|
@ -622,8 +723,12 @@ class DisplayMixin:
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if self.size:
|
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} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(
|
||||||
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(self)
|
self
|
||||||
|
)
|
||||||
|
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(
|
||||||
|
self
|
||||||
|
)
|
||||||
|
|
||||||
def __format__(self, format_spec: str) -> str:
|
def __format__(self, format_spec: str) -> str:
|
||||||
v = ''
|
v = ''
|
||||||
|
@ -645,6 +750,7 @@ class Computer(Device):
|
||||||
Computer is broadly extended by ``Desktop``, ``Laptop``, and
|
Computer is broadly extended by ``Desktop``, ``Laptop``, and
|
||||||
``Server``. The property ``chassis`` defines it more granularly.
|
``Server``. The property ``chassis`` defines it more granularly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
chassis = Column(DBEnum(ComputerChassis), nullable=True)
|
chassis = Column(DBEnum(ComputerChassis), nullable=True)
|
||||||
chassis.comment = """The physical form of the computer.
|
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.
|
It is a subset of the Linux definition of DMI / DMI decode.
|
||||||
"""
|
"""
|
||||||
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
|
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
|
||||||
owner_id = db.Column(UUID(as_uuid=True),
|
owner_id = db.Column(
|
||||||
db.ForeignKey(User.id),
|
UUID(as_uuid=True),
|
||||||
nullable=False,
|
db.ForeignKey(User.id),
|
||||||
default=lambda: g.user.id)
|
nullable=False,
|
||||||
|
default=lambda: g.user.id,
|
||||||
|
)
|
||||||
# author = db.relationship(User, primaryjoin=owner_id == 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__
|
transfer_state.comment = TransferState.__doc__
|
||||||
receiver_id = db.Column(UUID(as_uuid=True),
|
receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
|
||||||
db.ForeignKey(User.id),
|
|
||||||
nullable=True)
|
|
||||||
receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
|
receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
|
||||||
uuid = db.Column(UUID(as_uuid=True), nullable=True)
|
uuid = db.Column(UUID(as_uuid=True), nullable=True)
|
||||||
|
|
||||||
|
@ -685,22 +793,30 @@ class Computer(Device):
|
||||||
@property
|
@property
|
||||||
def ram_size(self) -> int:
|
def ram_size(self) -> int:
|
||||||
"""The total of RAM memory the computer has."""
|
"""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
|
@property
|
||||||
def data_storage_size(self) -> int:
|
def data_storage_size(self) -> int:
|
||||||
"""The total of data storage the computer has."""
|
"""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
|
@property
|
||||||
def processor_model(self) -> str:
|
def processor_model(self) -> str:
|
||||||
"""The model of one of the processors of the computer."""
|
"""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
|
@property
|
||||||
def graphic_card_model(self) -> str:
|
def graphic_card_model(self) -> str:
|
||||||
"""The model of one of the graphic cards of the computer."""
|
"""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
|
@property
|
||||||
def network_speeds(self) -> List[int]:
|
def network_speeds(self) -> List[int]:
|
||||||
|
@ -725,16 +841,18 @@ class Computer(Device):
|
||||||
it is not None.
|
it is not None.
|
||||||
"""
|
"""
|
||||||
return set(
|
return set(
|
||||||
privacy for privacy in
|
privacy
|
||||||
(hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage))
|
for privacy in (
|
||||||
|
hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage)
|
||||||
|
)
|
||||||
if privacy
|
if privacy
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def external_document_erasure(self):
|
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
|
from ereuse_devicehub.resources.action.models import DataWipe
|
||||||
|
|
||||||
urls = set()
|
urls = set()
|
||||||
try:
|
try:
|
||||||
ev = self.last_action_of(DataWipe)
|
ev = self.last_action_of(DataWipe)
|
||||||
|
@ -757,8 +875,11 @@ class Computer(Device):
|
||||||
if not self.hid:
|
if not self.hid:
|
||||||
return
|
return
|
||||||
components = self.components if components_snap is None else components_snap
|
components = self.components if components_snap is None else components_snap
|
||||||
macs_network = [c.serial_number for c in components
|
macs_network = [
|
||||||
if c.type == 'NetworkAdapter' and c.serial_number is not None]
|
c.serial_number
|
||||||
|
for c in components
|
||||||
|
if c.type == 'NetworkAdapter' and c.serial_number is not None
|
||||||
|
]
|
||||||
macs_network.sort()
|
macs_network.sort()
|
||||||
mac = macs_network[0] if macs_network else ''
|
mac = macs_network[0] if macs_network else ''
|
||||||
if not mac or mac in self.hid:
|
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 = db.Column(db.Integer, check_range('ram_size', min=128, max=36000))
|
||||||
ram_size.comment = """The total of RAM of the device in MB."""
|
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"""
|
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"""
|
display_size.comment = """The total size of the device screen"""
|
||||||
|
|
||||||
@validates('imei')
|
@validates('imei')
|
||||||
|
@ -856,21 +981,24 @@ class Cellphone(Mobile):
|
||||||
|
|
||||||
class Component(Device):
|
class Component(Device):
|
||||||
"""A device that can be inside another device."""
|
"""A device that can be inside another device."""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
|
|
||||||
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
||||||
parent = relationship(Computer,
|
parent = relationship(
|
||||||
backref=backref('components',
|
Computer,
|
||||||
lazy=True,
|
backref=backref(
|
||||||
cascade=CASCADE_DEL,
|
'components',
|
||||||
order_by=lambda: Component.id,
|
lazy=True,
|
||||||
collection_class=OrderedSet),
|
cascade=CASCADE_DEL,
|
||||||
primaryjoin=parent_id == Computer.id)
|
order_by=lambda: Component.id,
|
||||||
|
collection_class=OrderedSet,
|
||||||
__table_args__ = (
|
),
|
||||||
db.Index('parent_index', parent_id, postgresql_using='hash'),
|
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':
|
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
||||||
"""Gets a component that:
|
"""Gets a component that:
|
||||||
|
|
||||||
|
@ -882,11 +1010,16 @@ class Component(Device):
|
||||||
when looking for similar ones.
|
when looking for similar ones.
|
||||||
"""
|
"""
|
||||||
assert self.hid is None, 'Don\'t use this method with a component that has HID'
|
assert self.hid is None, 'Don\'t use this method with a component that has HID'
|
||||||
component = self.__class__.query \
|
component = (
|
||||||
.filter_by(parent=parent, hid=None, owner_id=self.owner_id,
|
self.__class__.query.filter_by(
|
||||||
**self.physical_properties) \
|
parent=parent,
|
||||||
.filter(~Component.id.in_(blacklist)) \
|
hid=None,
|
||||||
|
owner_id=self.owner_id,
|
||||||
|
**self.physical_properties,
|
||||||
|
)
|
||||||
|
.filter(~Component.id.in_(blacklist))
|
||||||
.first()
|
.first()
|
||||||
|
)
|
||||||
if not component:
|
if not component:
|
||||||
raise ResourceNotFound(self.type)
|
raise ResourceNotFound(self.type)
|
||||||
return component
|
return component
|
||||||
|
@ -909,7 +1042,8 @@ class GraphicCard(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
class DataStorage(JoinedComponentTableMixin, Component):
|
class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
"""A device that stores information."""
|
"""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."""
|
size.comment = """The size of the data-storage in MB."""
|
||||||
interface = Column(DBEnum(DataStorageInterface))
|
interface = Column(DBEnum(DataStorageInterface))
|
||||||
|
|
||||||
|
@ -920,6 +1054,7 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
This is, the last erasure performed to the data storage.
|
This is, the last erasure performed to the data storage.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.action.models import EraseBasic
|
from ereuse_devicehub.resources.action.models import EraseBasic
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ev = self.last_action_of(EraseBasic)
|
ev = self.last_action_of(EraseBasic)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
|
@ -934,9 +1069,9 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def external_document_erasure(self):
|
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
|
from ereuse_devicehub.resources.action.models import DataWipe
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ev = self.last_action_of(DataWipe)
|
ev = self.last_action_of(DataWipe)
|
||||||
return ev.document.url.to_text()
|
return ev.document.url.to_text()
|
||||||
|
@ -986,6 +1121,7 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
|
||||||
|
|
||||||
class Processor(JoinedComponentTableMixin, Component):
|
class Processor(JoinedComponentTableMixin, Component):
|
||||||
"""The CPU."""
|
"""The CPU."""
|
||||||
|
|
||||||
speed = Column(Float, check_range('speed', 0.1, 15))
|
speed = Column(Float, check_range('speed', 0.1, 15))
|
||||||
speed.comment = """The regular CPU speed."""
|
speed.comment = """The regular CPU speed."""
|
||||||
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
cores = Column(SmallInteger, check_range('cores', 1, 10))
|
||||||
|
@ -1000,6 +1136,7 @@ class Processor(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
class RamModule(JoinedComponentTableMixin, Component):
|
class RamModule(JoinedComponentTableMixin, Component):
|
||||||
"""A stick of RAM."""
|
"""A stick of RAM."""
|
||||||
|
|
||||||
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
size = Column(SmallInteger, check_range('size', min=128, max=17000))
|
||||||
size.comment = """The capacity of the RAM stick."""
|
size.comment = """The capacity of the RAM stick."""
|
||||||
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
|
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``
|
mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
|
||||||
and ``TelevisionSet``.
|
and ``TelevisionSet``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -1032,14 +1170,16 @@ class Battery(JoinedComponentTableMixin, Component):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capacity(self) -> float:
|
def capacity(self) -> float:
|
||||||
"""The quantity of """
|
"""The quantity of"""
|
||||||
from ereuse_devicehub.resources.action.models import MeasureBattery
|
from ereuse_devicehub.resources.action.models import MeasureBattery
|
||||||
|
|
||||||
real_size = self.last_action_of(MeasureBattery).size
|
real_size = self.last_action_of(MeasureBattery).size
|
||||||
return real_size / self.size if real_size and self.size else None
|
return real_size / self.size if real_size and self.size else None
|
||||||
|
|
||||||
|
|
||||||
class Camera(Component):
|
class Camera(Component):
|
||||||
"""The camera of a device."""
|
"""The camera of a device."""
|
||||||
|
|
||||||
focal_length = db.Column(db.SmallInteger)
|
focal_length = db.Column(db.SmallInteger)
|
||||||
video_height = db.Column(db.SmallInteger)
|
video_height = db.Column(db.SmallInteger)
|
||||||
video_width = db.Column(db.Integer)
|
video_width = db.Column(db.Integer)
|
||||||
|
@ -1052,6 +1192,7 @@ class Camera(Component):
|
||||||
|
|
||||||
class ComputerAccessory(Device):
|
class ComputerAccessory(Device):
|
||||||
"""Computer peripherals and similar accessories."""
|
"""Computer peripherals and similar accessories."""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1074,6 +1215,7 @@ class MemoryCardReader(ComputerAccessory):
|
||||||
|
|
||||||
class Networking(NetworkMixin, Device):
|
class Networking(NetworkMixin, Device):
|
||||||
"""Routers, switches, hubs..."""
|
"""Routers, switches, hubs..."""
|
||||||
|
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1119,6 +1261,7 @@ class Microphone(Sound):
|
||||||
|
|
||||||
class Video(Device):
|
class Video(Device):
|
||||||
"""Devices related to video treatment."""
|
"""Devices related to video treatment."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -1132,6 +1275,7 @@ class Videoconference(Video):
|
||||||
|
|
||||||
class Cooking(Device):
|
class Cooking(Device):
|
||||||
"""Cooking devices."""
|
"""Cooking devices."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -1183,6 +1327,7 @@ class Manufacturer(db.Model):
|
||||||
Ideally users should use the names from this list when submitting
|
Ideally users should use the names from this list when submitting
|
||||||
devices.
|
devices.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = db.Column(CIText(), primary_key=True)
|
name = db.Column(CIText(), primary_key=True)
|
||||||
name.comment = """The normalized name of the manufacturer."""
|
name.comment = """The normalized name of the manufacturer."""
|
||||||
url = db.Column(URL(), unique=True)
|
url = db.Column(URL(), unique=True)
|
||||||
|
@ -1193,7 +1338,7 @@ class Manufacturer(db.Model):
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
||||||
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
|
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
|
||||||
{'schema': 'common'}
|
{'schema': 'common'},
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1203,10 +1348,7 @@ class Manufacturer(db.Model):
|
||||||
#: Dialect used to write the CSV
|
#: Dialect used to write the CSV
|
||||||
|
|
||||||
with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f:
|
with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f:
|
||||||
cursor.copy_expert(
|
cursor.copy_expert('COPY common.manufacturer FROM STDIN (FORMAT csv)', f)
|
||||||
'COPY common.manufacturer FROM STDIN (FORMAT csv)',
|
|
||||||
f
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
listener_reset_field_updated_in_actual_time(Device)
|
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.
|
this tag is the same of devicehub_id.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.tag.model import Tag
|
from ereuse_devicehub.resources.tag.model import Tag
|
||||||
|
|
||||||
if isinstance(device, Computer):
|
if isinstance(device, Computer):
|
||||||
tag = Tag(device_id=device.id, id=device.devicehub_id)
|
tag = Tag(device_id=device.id, id=device.devicehub_id)
|
||||||
db.session.add(tag)
|
db.session.add(tag)
|
||||||
|
|
|
@ -29,6 +29,7 @@ class LotView(View):
|
||||||
"""
|
"""
|
||||||
format = EnumField(LotFormat, missing=None)
|
format = EnumField(LotFormat, missing=None)
|
||||||
search = f.Str(missing=None)
|
search = f.Str(missing=None)
|
||||||
|
type = f.Str(missing=None)
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
l = request.get_json()
|
l = request.get_json()
|
||||||
|
@ -88,6 +89,7 @@ class LotView(View):
|
||||||
else:
|
else:
|
||||||
query = Lot.query
|
query = Lot.query
|
||||||
query = self.visibility_filter(query)
|
query = self.visibility_filter(query)
|
||||||
|
query = self.type_filter(query, args)
|
||||||
if args['search']:
|
if args['search']:
|
||||||
query = query.filter(Lot.name.ilike(args['search'] + '%'))
|
query = query.filter(Lot.name.ilike(args['search'] + '%'))
|
||||||
lots = query.paginate(per_page=6 if args['search'] else query.count())
|
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))
|
Lot.owner_id == g.user.id))
|
||||||
return query
|
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):
|
def query(self, args):
|
||||||
query = Lot.query.distinct()
|
query = Lot.query.distinct()
|
||||||
return query
|
return query
|
||||||
|
|
|
@ -2,37 +2,44 @@ from uuid import uuid4
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from flask_login import UserMixin
|
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.dialects.postgresql import UUID
|
||||||
from sqlalchemy_utils import EmailType, PasswordType
|
from sqlalchemy_utils import EmailType, PasswordType
|
||||||
from teal.db import IntEnum
|
from teal.db import IntEnum
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
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.inventory.model import Inventory
|
||||||
from ereuse_devicehub.resources.models import STR_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_SIZE, Thing
|
||||||
from ereuse_devicehub.resources.enums import SessionType
|
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, Thing):
|
class User(UserMixin, Thing):
|
||||||
__table_args__ = {'schema': 'common'}
|
__table_args__ = {'schema': 'common'}
|
||||||
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
|
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
|
||||||
email = Column(EmailType, nullable=False, unique=True)
|
email = Column(EmailType, nullable=False, unique=True)
|
||||||
password = Column(PasswordType(max_length=STR_SIZE,
|
password = Column(
|
||||||
onload=lambda **kwargs: dict(
|
PasswordType(
|
||||||
schemes=app.config['PASSWORD_SCHEMES'],
|
max_length=STR_SIZE,
|
||||||
**kwargs
|
onload=lambda **kwargs: dict(
|
||||||
)))
|
schemes=app.config['PASSWORD_SCHEMES'], **kwargs
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||||
active = Column(Boolean, default=True, nullable=False)
|
active = Column(Boolean, default=True, nullable=False)
|
||||||
phantom = Column(Boolean, default=False, nullable=False)
|
phantom = Column(Boolean, default=False, nullable=False)
|
||||||
inventories = db.relationship(Inventory,
|
inventories = db.relationship(
|
||||||
backref=db.backref('users', lazy=True, collection_class=set),
|
Inventory,
|
||||||
secondary=lambda: UserInventory.__table__,
|
backref=db.backref('users', lazy=True, collection_class=set),
|
||||||
collection_class=set)
|
secondary=lambda: UserInventory.__table__,
|
||||||
|
collection_class=set,
|
||||||
|
)
|
||||||
|
|
||||||
# todo set restriction that user has, at least, one active db
|
# 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.
|
"""Creates an user.
|
||||||
:param email:
|
:param email:
|
||||||
:param password:
|
:param password:
|
||||||
|
@ -44,8 +51,13 @@ class User(UserMixin, Thing):
|
||||||
create during the trade actions
|
create during the trade actions
|
||||||
"""
|
"""
|
||||||
inventories = inventories or {Inventory.current}
|
inventories = inventories or {Inventory.current}
|
||||||
super().__init__(email=email, password=password, inventories=inventories,
|
super().__init__(
|
||||||
active=active, phantom=phantom)
|
email=email,
|
||||||
|
password=password,
|
||||||
|
inventories=inventories,
|
||||||
|
active=active,
|
||||||
|
phantom=phantom,
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<User {0.email}>'.format(self)
|
return '<User {0.email}>'.format(self)
|
||||||
|
@ -73,8 +85,8 @@ class User(UserMixin, Thing):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_full_name(self):
|
def get_full_name(self):
|
||||||
# TODO(@slamora) create first_name & last_name fields and use
|
# TODO(@slamora) create first_name & last_name fields???
|
||||||
# them to generate user full name
|
# needs to be discussed related to Agent <--> User concepts
|
||||||
return self.email
|
return self.email
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
|
@ -84,9 +96,12 @@ class User(UserMixin, Thing):
|
||||||
|
|
||||||
class UserInventory(db.Model):
|
class UserInventory(db.Model):
|
||||||
"""Relationship between users and their inventories."""
|
"""Relationship between users and their inventories."""
|
||||||
|
|
||||||
__table_args__ = {'schema': 'common'}
|
__table_args__ = {'schema': 'common'}
|
||||||
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id), primary_key=True)
|
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):
|
class Session(Thing):
|
||||||
|
@ -96,9 +111,11 @@ class Session(Thing):
|
||||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||||
type = Column(IntEnum(SessionType), default=SessionType.Internal, 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_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id))
|
||||||
user = db.relationship(User,
|
user = db.relationship(
|
||||||
backref=db.backref('sessions', lazy=True, collection_class=set),
|
User,
|
||||||
collection_class=set)
|
backref=db.backref('sessions', lazy=True, collection_class=set),
|
||||||
|
collection_class=set,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '{0.token}'.format(self)
|
return '{0.token}'.format(self)
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -19,12 +19,12 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #4154f1;
|
color: #6c757d ;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #717ff5;
|
color: #cc0066;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #012970;
|
color: #993365;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*--------------------------------------------------------------
|
/*--------------------------------------------------------------
|
||||||
|
@ -176,6 +176,31 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
opacity: 0;
|
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 */
|
/* Light Backgrounds */
|
||||||
.bg-primary-light {
|
.bg-primary-light {
|
||||||
background-color: #cfe2ff;
|
background-color: #cfe2ff;
|
||||||
|
@ -326,12 +351,12 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
color: #2c384e;
|
color: #2c384e;
|
||||||
}
|
}
|
||||||
.nav-tabs-bordered .nav-link:hover, .nav-tabs-bordered .nav-link:focus {
|
.nav-tabs-bordered .nav-link:hover, .nav-tabs-bordered .nav-link:focus {
|
||||||
color: #4154f1;
|
color: #993365;
|
||||||
}
|
}
|
||||||
.nav-tabs-bordered .nav-link.active {
|
.nav-tabs-bordered .nav-link.active {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #4154f1;
|
color: #993365;
|
||||||
border-bottom: 2px solid #4154f1;
|
border-bottom: 2px solid #993365;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*--------------------------------------------------------------
|
/*--------------------------------------------------------------
|
||||||
|
@ -370,7 +395,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #012970;
|
color: #993365;
|
||||||
}
|
}
|
||||||
.header .search-bar {
|
.header .search-bar {
|
||||||
min-width: 360px;
|
min-width: 360px;
|
||||||
|
@ -439,7 +464,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
color: #012970;
|
color: #012970;
|
||||||
}
|
}
|
||||||
.header-nav .nav-profile {
|
.header-nav .nav-profile {
|
||||||
color: #012970;
|
color: #993365;
|
||||||
}
|
}
|
||||||
.header-nav .nav-profile img {
|
.header-nav .nav-profile img {
|
||||||
max-height: 36px;
|
max-height: 36px;
|
||||||
|
@ -606,7 +631,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #4154f1;
|
color: #993365;
|
||||||
transition: 0.3;
|
transition: 0.3;
|
||||||
background: #f6f9ff;
|
background: #f6f9ff;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
|
@ -615,21 +640,21 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
.sidebar-nav .nav-link i {
|
.sidebar-nav .nav-link i {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
color: #4154f1;
|
color: #993365;
|
||||||
}
|
}
|
||||||
.sidebar-nav .nav-link.collapsed {
|
.sidebar-nav .nav-link.collapsed {
|
||||||
color: #012970;
|
color: #6c757d;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.sidebar-nav .nav-link.collapsed i {
|
.sidebar-nav .nav-link.collapsed i {
|
||||||
color: #899bbd;
|
color: #899bbd;
|
||||||
}
|
}
|
||||||
.sidebar-nav .nav-link:hover {
|
.sidebar-nav .nav-link:hover {
|
||||||
color: #4154f1;
|
color: #993365;
|
||||||
background: #f6f9ff;
|
background: #f6f9ff;
|
||||||
}
|
}
|
||||||
.sidebar-nav .nav-link:hover i {
|
.sidebar-nav .nav-link:hover i {
|
||||||
color: #4154f1;
|
color: #993365;
|
||||||
}
|
}
|
||||||
.sidebar-nav .nav-link .bi-chevron-down {
|
.sidebar-nav .nav-link .bi-chevron-down {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
@ -660,7 +685,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.sidebar-nav .nav-content a:hover, .sidebar-nav .nav-content a.active {
|
.sidebar-nav .nav-content a:hover, .sidebar-nav .nav-content a.active {
|
||||||
color: #4154f1;
|
color: #993365;
|
||||||
}
|
}
|
||||||
.sidebar-nav .nav-content a.active i {
|
.sidebar-nav .nav-content a.active i {
|
||||||
background-color: #4154f1;
|
background-color: #4154f1;
|
||||||
|
@ -1003,7 +1028,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
padding: 12px 15px;
|
padding: 12px 15px;
|
||||||
}
|
}
|
||||||
.contact .php-email-form button[type=submit] {
|
.contact .php-email-form button[type=submit] {
|
||||||
background: #4154f1;
|
background: #993365;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 10px 30px;
|
padding: 10px 30px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
@ -1011,7 +1036,15 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.contact .php-email-form button[type=submit]:hover {
|
.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 {
|
@-webkit-keyframes animate-loading {
|
||||||
0% {
|
0% {
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="188.02727mm"
|
||||||
|
height="48.976315mm"
|
||||||
|
viewBox="0 0 188.02727 48.976315"
|
||||||
|
version="1.1"
|
||||||
|
id="svg20276"
|
||||||
|
sodipodi:docname="logo_usody_clock.svg"
|
||||||
|
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview20278"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.82671156"
|
||||||
|
inkscape:cx="332.03842"
|
||||||
|
inkscape:cy="-66.528645"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-height="1013"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs20273" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-82.275303,-65.555746)">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-size:50.8px;line-height:1.25;font-family:sans-serif;stroke-width:0.264583"
|
||||||
|
x="126.75784"
|
||||||
|
y="103.84124"
|
||||||
|
id="text1904"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan1902"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583"
|
||||||
|
x="126.75784"
|
||||||
|
y="103.84124">Usod<tspan
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.8px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#993265;fill-opacity:1"
|
||||||
|
id="tspan8044">y</tspan></tspan></text>
|
||||||
|
<g
|
||||||
|
id="g14774-8"
|
||||||
|
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1"
|
||||||
|
transform="matrix(0.083748,0,0,0.08393574,81.384743,64.67498)">
|
||||||
|
<g
|
||||||
|
id="g14772-6"
|
||||||
|
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1">
|
||||||
|
<path
|
||||||
|
d="M 499.5,230.1 C 485.2,95.8 364.4,-2 230,12.4 157,20.2 92,59.8 51.5,121.1 c -6.2,9.4 -3.6,22.1 5.8,28.3 9.4,6.2 22.1,3.6 28.3,-5.8 33.8,-51.1 88,-84.1 148.8,-90.5 111.9,-11.9 212.6,69.4 224.5,181.3 5.8,54.2 -9.9,107.4 -44.2,149.9 -34.3,42.4 -83,69 -137.2,74.7 -74,8 -146.6,-25.6 -188.9,-86 l 37.7,8.1 c 11.1,2.3 21.9,-4.6 24.3,-15.7 2.4,-11 -4.6,-21.9 -15.7,-24.3 L 53.4,323.6 c -11,-2.4 -21.9,4.6 -24.3,15.7 l -17.5,81.5 c -2.4,11 4.7,21.7 15.7,24.3 12.4,2.9 22.2,-6.1 24.3,-15.7 L 57.9,400 c 46.1,63.3 120.2,101 198.3,101 8.5,0 17.1,-0.4 25.7,-1.4 65.1,-6.9 123.6,-38.8 164.7,-89.7 41,-50.8 59.8,-114.7 52.9,-179.8 z"
|
||||||
|
id="path14768-0"
|
||||||
|
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 271.8,140.3 c -11.3,0 -20.4,9.1 -20.4,20.4 V 256 c 0,5.4 2.2,10.6 6,14.4 l 95.3,95.3 c 9.5,10.7 24.9,4 28.9,0 8,-8 8,-20.9 0,-28.9 l -89.3,-89.3 v -86.8 c -0.1,-11.2 -9.2,-20.4 -20.5,-20.4 z"
|
||||||
|
id="path14770-4"
|
||||||
|
style="fill:#993264;fill-opacity:1;stroke:#993265;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g14772"
|
||||||
|
transform="matrix(0.083748,0,0,0.08393574,81.384743,64.67498)">
|
||||||
|
<path
|
||||||
|
id="path14768"
|
||||||
|
d="M 255.01758,10.996094 C 246.74651,11.038013 238.4,11.500391 230,12.400391 c -73,7.8 -138,47.399218 -178.5,108.699219 -6.2,9.4 -3.599219,22.10078 5.800781,28.30078 9.4,6.2 22.098828,3.59922 28.298828,-5.80078 33.800001,-51.100001 88.000781,-84.100001 148.800781,-90.500001 111.9,-11.9 212.6,69.400781 224.5,181.300781 5.8,54.2 -9.90117,107.40039 -44.20117,149.90039 C 380.39922,426.70078 331.7,453.3 277.5,459 c -8.97135,0.96988 -17.92013,1.30083 -26.79883,1.07422 l 0.0449,40.76172 c 1.81975,0.041 3.62871,0.16406 5.45313,0.16406 8.5,0 17.10117,-0.40039 25.70117,-1.40039 65.1,-6.9 123.59922,-38.79922 164.69922,-89.69922 41,-50.8 59.80039,-114.70078 52.90039,-179.80078 C 486.09375,104.19336 379.08362,10.36731 255.01758,10.996094 Z" />
|
||||||
|
<path
|
||||||
|
d="m 271.8,140.3 c -11.3,0 -20.4,9.1 -20.4,20.4 V 256 c 0,5.4 2.2,10.6 6,14.4 l 95.3,95.3 c 9.5,10.7 24.9,4 28.9,0 8,-8 8,-20.9 0,-28.9 l -89.3,-89.3 v -86.8 c -0.1,-11.2 -9.2,-20.4 -20.5,-20.4 z"
|
||||||
|
id="path14770" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.7 KiB |
|
@ -4,7 +4,7 @@ const Api = {
|
||||||
* @returns get lots
|
* @returns get lots
|
||||||
*/
|
*/
|
||||||
async 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;
|
if (request != undefined) return request.items;
|
||||||
throw request;
|
throw request;
|
||||||
},
|
},
|
||||||
|
@ -15,7 +15,7 @@ const Api = {
|
||||||
* @returns full detailed device list
|
* @returns full detailed device list
|
||||||
*/
|
*/
|
||||||
async get_devices(ids) {
|
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;
|
if (request != undefined) return request.items;
|
||||||
throw request;
|
throw request;
|
||||||
},
|
},
|
||||||
|
@ -26,7 +26,7 @@ const Api = {
|
||||||
* @returns full detailed device list
|
* @returns full detailed device list
|
||||||
*/
|
*/
|
||||||
async search_device(id) {
|
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
|
if (request != undefined) return request.items
|
||||||
throw request
|
throw request
|
||||||
},
|
},
|
||||||
|
@ -37,8 +37,8 @@ const Api = {
|
||||||
* @param {number[]} listDevices list devices id
|
* @param {number[]} listDevices list devices id
|
||||||
*/
|
*/
|
||||||
async devices_add(lotID, listDevices) {
|
async devices_add(lotID, listDevices) {
|
||||||
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&");
|
const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`;
|
||||||
return await Api.doRequest(queryURL, "POST", null);
|
return Api.doRequest(queryURL, "POST", null);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,8 +47,8 @@ const Api = {
|
||||||
* @param {number[]} listDevices list devices id
|
* @param {number[]} listDevices list devices id
|
||||||
*/
|
*/
|
||||||
async devices_remove(lotID, listDevices) {
|
async devices_remove(lotID, listDevices) {
|
||||||
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&");
|
const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`;
|
||||||
return await Api.doRequest(queryURL, "DELETE", null);
|
return Api.doRequest(queryURL, "DELETE", null);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,13 +59,13 @@ const Api = {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async doRequest(url, type, body) {
|
async doRequest(url, type, body) {
|
||||||
var result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await $.ajax({
|
result = await $.ajax({
|
||||||
url: url,
|
url,
|
||||||
type: type,
|
type,
|
||||||
headers: { "Authorization": API_URLS.Auth_Token },
|
headers: { "Authorization": API_URLS.Auth_Token },
|
||||||
body: body
|
body
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
$(document).ready(function() {
|
$(document).ready(() => {
|
||||||
$("#type").on("change", deviceInputs);
|
$("#type").on("change", deviceInputs);
|
||||||
deviceInputs();
|
deviceInputs();
|
||||||
})
|
})
|
||||||
|
|
||||||
function deviceInputs() {
|
function deviceInputs() {
|
||||||
if ($("#type").val() == 'Monitor') {
|
if ($("#type").val() == "Monitor") {
|
||||||
$("#screen").show();
|
$("#screen").show();
|
||||||
$("#resolution").show();
|
$("#resolution").show();
|
||||||
$("#imei").hide();
|
$("#imei").hide();
|
||||||
$("#meid").hide();
|
$("#meid").hide();
|
||||||
} else if (['Smartphone', 'Cellphone', 'Tablet'].includes($("#type").val())) {
|
} else if (["Smartphone", "Cellphone", "Tablet"].includes($("#type").val())) {
|
||||||
$("#screen").hide();
|
$("#screen").hide();
|
||||||
$("#resolution").hide();
|
$("#resolution").hide();
|
||||||
$("#imei").show();
|
$("#imei").show();
|
||||||
|
|
|
@ -14,9 +14,9 @@
|
||||||
el = el.trim()
|
el = el.trim()
|
||||||
if (all) {
|
if (all) {
|
||||||
return [...document.querySelectorAll(el)]
|
return [...document.querySelectorAll(el)]
|
||||||
} else {
|
|
||||||
return document.querySelector(el)
|
|
||||||
}
|
}
|
||||||
|
return document.querySelector(el)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,103 +34,101 @@
|
||||||
* Easy on scroll event listener
|
* Easy on scroll event listener
|
||||||
*/
|
*/
|
||||||
const onscroll = (el, listener) => {
|
const onscroll = (el, listener) => {
|
||||||
el.addEventListener('scroll', listener)
|
el.addEventListener("scroll", listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sidebar toggle
|
* Sidebar toggle
|
||||||
*/
|
*/
|
||||||
if (select('.toggle-sidebar-btn')) {
|
if (select(".toggle-sidebar-btn")) {
|
||||||
on('click', '.toggle-sidebar-btn', function (e) {
|
on("click", ".toggle-sidebar-btn", (e) => {
|
||||||
select('body').classList.toggle('toggle-sidebar')
|
select("body").classList.toggle("toggle-sidebar")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search bar toggle
|
* Search bar toggle
|
||||||
*/
|
*/
|
||||||
if (select('.search-bar-toggle')) {
|
if (select(".search-bar-toggle")) {
|
||||||
on('click', '.search-bar-toggle', function (e) {
|
on("click", ".search-bar-toggle", (e) => {
|
||||||
select('.search-bar').classList.toggle('search-bar-show')
|
select(".search-bar").classList.toggle("search-bar-show")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navbar links active state on scroll
|
* Navbar links active state on scroll
|
||||||
*/
|
*/
|
||||||
let navbarlinks = select('#navbar .scrollto', true)
|
const navbarlinks = select("#navbar .scrollto", true)
|
||||||
const navbarlinksActive = () => {
|
const navbarlinksActive = () => {
|
||||||
let position = window.scrollY + 200
|
const position = window.scrollY + 200
|
||||||
navbarlinks.forEach(navbarlink => {
|
navbarlinks.forEach(navbarlink => {
|
||||||
if (!navbarlink.hash) return
|
if (!navbarlink.hash) return
|
||||||
let section = select(navbarlink.hash)
|
const section = select(navbarlink.hash)
|
||||||
if (!section) return
|
if (!section) return
|
||||||
if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
|
if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
|
||||||
navbarlink.classList.add('active')
|
navbarlink.classList.add("active")
|
||||||
} else {
|
} else {
|
||||||
navbarlink.classList.remove('active')
|
navbarlink.classList.remove("active")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
window.addEventListener('load', navbarlinksActive)
|
window.addEventListener("load", navbarlinksActive)
|
||||||
onscroll(document, navbarlinksActive)
|
onscroll(document, navbarlinksActive)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle .header-scrolled class to #header when page is scrolled
|
* Toggle .header-scrolled class to #header when page is scrolled
|
||||||
*/
|
*/
|
||||||
let selectHeader = select('#header')
|
const selectHeader = select("#header")
|
||||||
if (selectHeader) {
|
if (selectHeader) {
|
||||||
const headerScrolled = () => {
|
const headerScrolled = () => {
|
||||||
if (window.scrollY > 100) {
|
if (window.scrollY > 100) {
|
||||||
selectHeader.classList.add('header-scrolled')
|
selectHeader.classList.add("header-scrolled")
|
||||||
} else {
|
} else {
|
||||||
selectHeader.classList.remove('header-scrolled')
|
selectHeader.classList.remove("header-scrolled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('load', headerScrolled)
|
window.addEventListener("load", headerScrolled)
|
||||||
onscroll(document, headerScrolled)
|
onscroll(document, headerScrolled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Back to top button
|
* Back to top button
|
||||||
*/
|
*/
|
||||||
let backtotop = select('.back-to-top')
|
const backtotop = select(".back-to-top")
|
||||||
if (backtotop) {
|
if (backtotop) {
|
||||||
const toggleBacktotop = () => {
|
const toggleBacktotop = () => {
|
||||||
if (window.scrollY > 100) {
|
if (window.scrollY > 100) {
|
||||||
backtotop.classList.add('active')
|
backtotop.classList.add("active")
|
||||||
} else {
|
} else {
|
||||||
backtotop.classList.remove('active')
|
backtotop.classList.remove("active")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('load', toggleBacktotop)
|
window.addEventListener("load", toggleBacktotop)
|
||||||
onscroll(document, toggleBacktotop)
|
onscroll(document, toggleBacktotop)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate tooltips
|
* Initiate tooltips
|
||||||
*/
|
*/
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
|
||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
const tooltipList = tooltipTriggerList.map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate quill editors
|
* Initiate quill editors
|
||||||
*/
|
*/
|
||||||
if (select('.quill-editor-default')) {
|
if (select(".quill-editor-default")) {
|
||||||
new Quill('.quill-editor-default', {
|
new Quill(".quill-editor-default", {
|
||||||
theme: 'snow'
|
theme: "snow"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (select('.quill-editor-bubble')) {
|
if (select(".quill-editor-bubble")) {
|
||||||
new Quill('.quill-editor-bubble', {
|
new Quill(".quill-editor-bubble", {
|
||||||
theme: 'bubble'
|
theme: "bubble"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (select('.quill-editor-full')) {
|
if (select(".quill-editor-full")) {
|
||||||
new Quill(".quill-editor-full", {
|
new Quill(".quill-editor-full", {
|
||||||
modules: {
|
modules: {
|
||||||
toolbar: [
|
toolbar: [
|
||||||
|
@ -181,24 +179,24 @@
|
||||||
/**
|
/**
|
||||||
* Initiate Bootstrap validation check
|
* Initiate Bootstrap validation check
|
||||||
*/
|
*/
|
||||||
var needsValidation = document.querySelectorAll('.needs-validation')
|
const needsValidation = document.querySelectorAll(".needs-validation")
|
||||||
|
|
||||||
Array.prototype.slice.call(needsValidation)
|
Array.prototype.slice.call(needsValidation)
|
||||||
.forEach(function (form) {
|
.forEach((form) => {
|
||||||
form.addEventListener('submit', function (event) {
|
form.addEventListener("submit", (event) => {
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
|
|
||||||
form.classList.add('was-validated')
|
form.classList.add("was-validated")
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate Datatables
|
* Initiate Datatables
|
||||||
*/
|
*/
|
||||||
const datatables = select('.datatable', true)
|
const datatables = select(".datatable", true)
|
||||||
datatables.forEach(datatable => {
|
datatables.forEach(datatable => {
|
||||||
new simpleDatatables.DataTable(datatable);
|
new simpleDatatables.DataTable(datatable);
|
||||||
})
|
})
|
||||||
|
@ -206,45 +204,17 @@
|
||||||
/**
|
/**
|
||||||
* Autoresize echart charts
|
* Autoresize echart charts
|
||||||
*/
|
*/
|
||||||
const mainContainer = select('#main');
|
const mainContainer = select("#main");
|
||||||
if (mainContainer) {
|
if (mainContainer) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
new ResizeObserver(function () {
|
new ResizeObserver(() => {
|
||||||
select('.echart', true).forEach(getEchart => {
|
select(".echart", true).forEach(getEchart => {
|
||||||
echarts.getInstanceByDom(getEchart).resize();
|
echarts.getInstanceByDom(getEchart).resize();
|
||||||
})
|
})
|
||||||
}).observe(mainContainer);
|
}).observe(mainContainer);
|
||||||
}, 200);
|
}, 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
|
* Avoid hide dropdown when user clicked inside
|
||||||
*/
|
*/
|
||||||
|
@ -256,23 +226,23 @@
|
||||||
* Search form functionality
|
* Search form functionality
|
||||||
*/
|
*/
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
var searchForm = document.getElementById("SearchForm")
|
const searchForm = document.getElementById("SearchForm")
|
||||||
var inputSearch = document.querySelector("#SearchForm > input")
|
const inputSearch = document.querySelector("#SearchForm > input")
|
||||||
var doSearch = true
|
const doSearch = true
|
||||||
|
|
||||||
searchForm.addEventListener("submit", (event) => {
|
searchForm.addEventListener("submit", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
})
|
})
|
||||||
|
|
||||||
let timeoutHandler = setTimeout(() => { }, 1)
|
let timeoutHandler = setTimeout(() => { }, 1)
|
||||||
let dropdownList = document.getElementById("dropdown-search-list")
|
const dropdownList = document.getElementById("dropdown-search-list")
|
||||||
let defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
|
const defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
|
||||||
|
|
||||||
|
|
||||||
inputSearch.addEventListener("input", (e) => {
|
inputSearch.addEventListener("input", (e) => {
|
||||||
clearTimeout(timeoutHandler)
|
clearTimeout(timeoutHandler)
|
||||||
let searchText = e.target.value
|
const searchText = e.target.value
|
||||||
if (searchText == '') {
|
if (searchText == "") {
|
||||||
document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch;
|
document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch;
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -315,7 +285,7 @@
|
||||||
const device = devices[i];
|
const device = devices[i];
|
||||||
|
|
||||||
// See: ereuse_devicehub/resources/device/models.py
|
// 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 = `
|
const templateString = `
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
$(document).ready(function() {
|
$(document).ready(() => {
|
||||||
var show_allocate_form = $("#allocateModal").data('show-action-form');
|
const show_allocate_form = $("#allocateModal").data("show-action-form");
|
||||||
var show_datawipe_form = $("#datawipeModal").data('show-action-form');
|
const show_datawipe_form = $("#datawipeModal").data("show-action-form");
|
||||||
var show_trade_form = $("#tradeLotModal").data('show-action-form');
|
const show_trade_form = $("#tradeLotModal").data("show-action-form");
|
||||||
if (show_allocate_form != "None") {
|
if (show_allocate_form != "None") {
|
||||||
$("#allocateModal .btn-primary").show();
|
$("#allocateModal .btn-primary").show();
|
||||||
newAllocate(show_allocate_form);
|
newAllocate(show_allocate_form);
|
||||||
|
@ -17,8 +17,119 @@ $(document).ready(function() {
|
||||||
// $('#selectLot').selectpicker();
|
// $('#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
|
||||||
|
? `<a href="#" class="ml-3">Select all devices (${TableController.getAllDevices().length})</a>`
|
||||||
|
: "<a href=\"#\" class=\"ml-3\">Cancel selection</a>"
|
||||||
|
}`;
|
||||||
|
alertInfoDevices.classList.remove("d-none");
|
||||||
|
} else if (isAllChecked.every(bool => bool == false)) {
|
||||||
|
btnSelectAll.checked = false;
|
||||||
|
btnSelectAll.indeterminate = false;
|
||||||
|
alertInfoDevices.classList.add("d-none")
|
||||||
|
} else {
|
||||||
|
btnSelectAll.indeterminate = true;
|
||||||
|
alertInfoDevices.classList.add("d-none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
function deviceSelect() {
|
||||||
var devices_count = $(".deviceSelect").filter(':checked').length;
|
const devices_count = TableController.getSelectedDevices().length;
|
||||||
get_device_list();
|
get_device_list();
|
||||||
if (devices_count == 0) {
|
if (devices_count == 0) {
|
||||||
$("#addingLotModal .pol").show();
|
$("#addingLotModal .pol").show();
|
||||||
|
@ -60,7 +171,7 @@ function deviceSelect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeLot() {
|
function removeLot() {
|
||||||
var devices = $(".deviceSelect");
|
const devices = TableController.getAllDevices();
|
||||||
if (devices.length > 0) {
|
if (devices.length > 0) {
|
||||||
$("#btnRemoveLots .text-danger").show();
|
$("#btnRemoveLots .text-danger").show();
|
||||||
} else {
|
} else {
|
||||||
|
@ -70,10 +181,10 @@ function removeLot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTag() {
|
function removeTag() {
|
||||||
var devices = $(".deviceSelect").filter(':checked');
|
const devices = TableController.getSelectedDevices();
|
||||||
var devices_id = $.map(devices, function(x) { return $(x).attr('data')});
|
const devices_id = devices.map(dev => dev.data);
|
||||||
if (devices_id.length == 1) {
|
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;
|
window.location.href = url;
|
||||||
} else {
|
} else {
|
||||||
$("#unlinkTagAlertModal").click();
|
$("#unlinkTagAlertModal").click();
|
||||||
|
@ -81,8 +192,8 @@ function removeTag() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTag() {
|
function addTag() {
|
||||||
var devices = $(".deviceSelect").filter(':checked');
|
const devices = TableController.getSelectedDevices();
|
||||||
var devices_id = $.map(devices, function(x) { return $(x).attr('data')});
|
const devices_id = devices.map(dev => dev.data);
|
||||||
if (devices_id.length == 1) {
|
if (devices_id.length == 1) {
|
||||||
$("#addingTagModal .pol").hide();
|
$("#addingTagModal .pol").hide();
|
||||||
$("#addingTagModal .btn-primary").show();
|
$("#addingTagModal .btn-primary").show();
|
||||||
|
@ -95,20 +206,20 @@ function addTag() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function newTrade(action) {
|
function newTrade(action) {
|
||||||
var title = "Trade "
|
let title = "Trade "
|
||||||
var user_to = $("#user_to").data("email");
|
const user_to = $("#user_to").data("email");
|
||||||
var user_from = $("#user_from").data("email");
|
const user_from = $("#user_from").data("email");
|
||||||
if (action == 'user_from') {
|
if (action == "user_from") {
|
||||||
title = 'Trade Incoming';
|
title = "Trade Incoming";
|
||||||
$("#user_to").attr('readonly', 'readonly');
|
$("#user_to").attr("readonly", "readonly");
|
||||||
$("#user_from").prop('readonly', false);
|
$("#user_from").prop("readonly", false);
|
||||||
$("#user_from").val('');
|
$("#user_from").val("");
|
||||||
$("#user_to").val(user_to);
|
$("#user_to").val(user_to);
|
||||||
} else if (action == 'user_to') {
|
} else if (action == "user_to") {
|
||||||
title = 'Trade Outgoing';
|
title = "Trade Outgoing";
|
||||||
$("#user_from").attr('readonly', 'readonly');
|
$("#user_from").attr("readonly", "readonly");
|
||||||
$("#user_to").prop('readonly', false);
|
$("#user_to").prop("readonly", false);
|
||||||
$("#user_to").val('');
|
$("#user_to").val("");
|
||||||
$("#user_from").val(user_from);
|
$("#user_from").val(user_from);
|
||||||
}
|
}
|
||||||
$("#tradeLotModal #title-action").html(title);
|
$("#tradeLotModal #title-action").html(title);
|
||||||
|
@ -137,44 +248,45 @@ function newDataWipe(action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_device_list() {
|
function get_device_list() {
|
||||||
var devices = $(".deviceSelect").filter(':checked');
|
const devices = TableController.getSelectedDevices();
|
||||||
|
|
||||||
/* Insert the correct count of devices in actions form */
|
/* 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);
|
$("#datawipeModal .devices-count").html(devices_count);
|
||||||
$("#allocateModal .devices-count").html(devices_count);
|
$("#allocateModal .devices-count").html(devices_count);
|
||||||
$("#actionModal .devices-count").html(devices_count);
|
$("#actionModal .devices-count").html(devices_count);
|
||||||
|
|
||||||
/* Insert the correct value in the input devicesList */
|
/* Insert the correct value in the input devicesList */
|
||||||
var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(",");
|
const devices_id = $.map(devices, (x) => $(x).attr("data")).join(",");
|
||||||
$.map($(".devicesList"), function(x) {
|
$.map($(".devicesList"), (x) => {
|
||||||
$(x).val(devices_id);
|
$(x).val(devices_id);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Create a list of devices for human representation */
|
/* Create a list of devices for human representation */
|
||||||
var computer = {
|
const computer = {
|
||||||
"Desktop": "<i class='bi bi-building'></i>",
|
"Desktop": "<i class='bi bi-building'></i>",
|
||||||
"Laptop": "<i class='bi bi-laptop'></i>",
|
"Laptop": "<i class='bi bi-laptop'></i>",
|
||||||
};
|
};
|
||||||
list_devices = devices.map(function (x) {
|
|
||||||
var typ = $(devices[x]).data("device-type");
|
list_devices = devices.map((x) => {
|
||||||
var manuf = $(devices[x]).data("device-manufacturer");
|
let typ = $(x).data("device-type");
|
||||||
var dhid = $(devices[x]).data("device-dhid");
|
const manuf = $(x).data("device-manufacturer");
|
||||||
|
const dhid = $(x).data("device-dhid");
|
||||||
if (computer[typ]) {
|
if (computer[typ]) {
|
||||||
typ = 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);
|
$(".enumeration-devices").html(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
function export_file(type_file) {
|
function export_file(type_file) {
|
||||||
var devices = $(".deviceSelect").filter(':checked');
|
const devices = TableController.getSelectedDevices();
|
||||||
var devices_id = $.map(devices, function(x) { return $(x).attr('data-device-dhid')}).join(",");
|
const devices_id = $.map(devices, (x) => $(x).attr("data-device-dhid")).join(",");
|
||||||
if (devices_id){
|
if (devices_id) {
|
||||||
var url = "/inventory/export/"+type_file+"/?ids="+devices_id;
|
const url = `/inventory/export/${type_file}/?ids=${devices_id}`;
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
} else {
|
} else {
|
||||||
$("#exportAlertModal").click();
|
$("#exportAlertModal").click();
|
||||||
|
@ -194,40 +306,28 @@ async function processSelectedDevices() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage the actions that will be performed when applying the changes
|
* Manage the actions that will be performed when applying the changes
|
||||||
* @param {*} ev event (Should be a checkbox type)
|
* @param {EventSource} ev event (Should be a checkbox type)
|
||||||
* @param {string} lotID lot id
|
* @param {Lot} lot lot id
|
||||||
* @param {number} deviceID device id
|
* @param {Device[]} selectedDevices device id
|
||||||
*/
|
*/
|
||||||
manage(event, lotID, deviceListID) {
|
manage(event, lot, selectedDevices) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const indeterminate = event.srcElement.indeterminate;
|
const lotID = lot.id;
|
||||||
const checked = !event.srcElement.checked;
|
const srcElement = event.srcElement.parentElement.children[0]
|
||||||
|
const checked = !srcElement.checked;
|
||||||
|
|
||||||
var found = this.list.filter(list => list.lotID == lotID)[0];
|
const found = this.list.filter(list => list.lot.id == lotID)[0];
|
||||||
var foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1;
|
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (found != undefined && found.type == "Remove") {
|
if (found && found.type == "Remove") {
|
||||||
if (found.isFromIndeterminate == true) {
|
found.type = "Add";
|
||||||
found.type = "Add";
|
|
||||||
this.list[foundIndex] = found;
|
|
||||||
} else {
|
|
||||||
this.list = this.list.filter(list => list.lotID != lotID);
|
|
||||||
}
|
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
if (found != undefined && found.type == "Add") {
|
this.list.push({ type: "Remove", lot, devices: selectedDevices });
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.list.length > 0) {
|
if (this.list.length > 0) {
|
||||||
|
@ -244,10 +344,10 @@ async function processSelectedDevices() {
|
||||||
* @param {boolean} isError defines if a toast is a error
|
* @param {boolean} isError defines if a toast is a error
|
||||||
*/
|
*/
|
||||||
notifyUser(title, toastText, isError) {
|
notifyUser(title, toastText, isError) {
|
||||||
let toast = document.createElement("div");
|
const toast = document.createElement("div");
|
||||||
toast.classList = "alert alert-dismissible fade show " + (isError ? "alert-danger" : "alert-success");
|
toast.classList = `alert alert-dismissible fade show ${isError ? "alert-danger" : "alert-success"}`;
|
||||||
toast.attributes["data-autohide"] = !isError;
|
toast.attributes["data-autohide"] = !isError;
|
||||||
toast.attributes["role"] = "alert";
|
toast.attributes.role = "alert";
|
||||||
toast.style = "margin-left: auto; width: fit-content;";
|
toast.style = "margin-left: auto; width: fit-content;";
|
||||||
toast.innerHTML = `<strong>${title}</strong><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`;
|
toast.innerHTML = `<strong>${title}</strong><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`;
|
||||||
if (toastText && toastText.length > 0) {
|
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
|
* Get actions and execute call request to add or remove devices from lots
|
||||||
*/
|
*/
|
||||||
doActions() {
|
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 => {
|
this.list.forEach(async action => {
|
||||||
if (action.type == "Add") {
|
if (action.type == "Add") {
|
||||||
try {
|
try {
|
||||||
await Api.devices_add(action.lotID, action.devices);
|
const devicesIDs = action.devices.filter(dev => !action.lot.devices.includes(dev.id)).map(dev => dev.id)
|
||||||
this.notifyUser("Devices sucefully aded to selected lot/s", "", false);
|
await Api.devices_add(action.lot.id, devicesIDs);
|
||||||
|
this.notifyUser("Devices sucefully added to selected lot/s", "", false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
|
this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
|
||||||
}
|
}
|
||||||
} else if (action.type == "Remove") {
|
} else if (action.type == "Remove") {
|
||||||
try {
|
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);
|
this.notifyUser("Devices sucefully removed from selected lot/s", "", false);
|
||||||
} catch (error) {
|
} 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
|
requestCount += 1
|
||||||
|
@ -288,6 +390,7 @@ async function processSelectedDevices() {
|
||||||
this.list = [];
|
this.list = [];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
$("#confirmLotsModal").modal("hide"); // Hide dialog when click "Save changes"
|
||||||
document.getElementById("dropDownLotsSelector").classList.remove("show");
|
document.getElementById("dropDownLotsSelector").classList.remove("show");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,90 +398,163 @@ async function processSelectedDevices() {
|
||||||
* Re-render list in table
|
* Re-render list in table
|
||||||
*/
|
*/
|
||||||
async reRenderTable() {
|
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
|
tmpDiv.innerHTML = newRequest
|
||||||
|
|
||||||
var oldTable = Array.from(document.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)
|
||||||
var 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++) {
|
// https://github.com/fiduswriter/Simple-DataTables/wiki/rows()#removeselect-arraynumber
|
||||||
if (!newTable.includes(oldTable[i])) {
|
const rowsToRemove = []
|
||||||
// variable from device_list.html --> See: ereuse_devicehub\templates\inventory\device_list.html (Ln: 411)
|
for (let i = 0; i < table.activeRows.length; i++) {
|
||||||
table.rows().remove(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
|
* Generates a list item with a correspondient checkbox state
|
||||||
* @param {String} lotID
|
* @param {Object} lot Lot model server
|
||||||
* @param {String} lotName
|
* @param {Device[]} selectedDevices list selected devices
|
||||||
* @param {Array<number>} selectedDevicesIDs
|
* @param {HTMLElement} elementTarget
|
||||||
* @param {HTMLElement} target
|
* @param {Action[]} actions
|
||||||
*/
|
*/
|
||||||
function templateLot(lotID, lot, selectedDevicesIDs, elementTarget, actions) {
|
function templateLot(lot, selectedDevices, elementTarget, actions) {
|
||||||
elementTarget.innerHTML = ""
|
elementTarget.innerHTML = ""
|
||||||
|
const { id, name, state } = lot;
|
||||||
|
|
||||||
var htmlTemplate = `<input class="form-check-input" type="checkbox" id="${lotID}" style="width: 20px; height: 20px; margin-right: 7px;">
|
const htmlTemplate = `<input class="form-check-input" type="checkbox" id="${id}" style="width: 20px; height: 20px; margin-right: 7px;">
|
||||||
<label class="form-check-label" for="${lotID}">${lot.name}</label>`;
|
<label class="form-check-label" for="${id}">${name}</label>`;
|
||||||
|
|
||||||
var existLotList = selectedDevicesIDs.map(selected => lot.devices.includes(selected));
|
const doc = document.createElement("li");
|
||||||
|
|
||||||
var doc = document.createElement('li');
|
|
||||||
doc.innerHTML = htmlTemplate;
|
doc.innerHTML = htmlTemplate;
|
||||||
|
|
||||||
if (selectedDevicesIDs.length <= 0) {
|
switch (state) {
|
||||||
doc.children[0].disabled = true;
|
case "true":
|
||||||
} else if (existLotList.every(value => value == true)) {
|
doc.children[0].checked = true;
|
||||||
doc.children[0].checked = true;
|
break;
|
||||||
} else if (existLotList.every(value => value == false)) {
|
case "false":
|
||||||
doc.children[0].checked = false;
|
doc.children[0].checked = false;
|
||||||
} else {
|
break;
|
||||||
doc.children[0].indeterminate = true;
|
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);
|
elementTarget.append(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
var listHTML = $("#LotsSelector")
|
const listHTML = $("#LotsSelector")
|
||||||
|
|
||||||
// Get selected devices
|
// Get selected devices
|
||||||
var selectedDevicesIDs = $.map($(".deviceSelect").filter(':checked'), function (x) { return parseInt($(x).attr('data')) });
|
const selectedDevicesID = TableController.ProcessTR(TableController.getSelectedDevices()).map(item => item.data)
|
||||||
if (selectedDevicesIDs.length <= 0) {
|
|
||||||
listHTML.html('<li style="color: red; text-align: center">No devices selected</li>');
|
if (selectedDevicesID.length <= 0) {
|
||||||
|
listHTML.html("<li style=\"color: red; text-align: center\">No devices selected</li>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Actions list, and set checkbox triggers
|
// Initialize Actions list, and set checkbox triggers
|
||||||
var actions = new Actions();
|
const actions = new Actions();
|
||||||
if (eventClickActions) {
|
if (eventClickActions) {
|
||||||
document.getElementById("ApplyDeviceLots").removeEventListener(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 += `
|
||||||
|
<div class="card border-primary mb-3 w-100">
|
||||||
|
<div class="card-header" title="${action.lotID}">${action.lot.name}</div>
|
||||||
|
<div class="card-body pt-3">
|
||||||
|
<p class="card-text">
|
||||||
|
${devices.map(item => {
|
||||||
|
const name = `${item.type} ${item.manufacturer} ${item.model}`
|
||||||
|
return `<span class="badge bg-${type}" title="${name}">${item.devicehubID}</span>`;
|
||||||
|
}).join(" ")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
|
||||||
|
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");
|
document.getElementById("ApplyDeviceLots").classList.add("disabled");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
listHTML.html('<li style="text-align: center"><div class="spinner-border text-info" style="margin: auto" role="status"></div></li>')
|
listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>")
|
||||||
var devices = await Api.get_devices(selectedDevicesIDs);
|
const selectedDevices = await Api.get_devices(selectedDevicesID);
|
||||||
var lots = await Api.get_lots();
|
let lots = await Api.get_lots();
|
||||||
|
|
||||||
lots = lots.map(lot => {
|
lots = lots.map(lot => {
|
||||||
lot.devices = devices
|
lot.devices = selectedDevices
|
||||||
.filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0)
|
.filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0)
|
||||||
.map(device => parseInt(device.id));
|
.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;
|
return lot;
|
||||||
})
|
})
|
||||||
|
|
||||||
listHTML.html('');
|
let lotsList = [];
|
||||||
lots.forEach(lot => templateLot(lot.id, lot, selectedDevicesIDs, listHTML, actions));
|
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) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
listHTML.html('<li style="color: red; text-align: center">Error feching devices and lots<br>(see console for more details)</li>');
|
listHTML.html("<li style=\"color: red; text-align: center\">Error feching devices and lots<br>(see console for more details)</li>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
STORAGE_KEY = 'tag-spec-key';
|
STORAGE_KEY = 'tag-spec-key';
|
||||||
$("#printerType").on("change", change_size);
|
$("#printerType").on("change", change_size);
|
||||||
|
$(".form-check-input").on("change", change_check);
|
||||||
change_size();
|
change_size();
|
||||||
load_size();
|
load_settings();
|
||||||
|
change_check();
|
||||||
})
|
})
|
||||||
|
|
||||||
function qr_draw(url, id) {
|
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 height = $("#height-tag").val();
|
||||||
var width = $("#width-tag").val();
|
var width = $("#width-tag").val();
|
||||||
var sizePreset = $("#printerType").val();
|
var sizePreset = $("#printerType").val();
|
||||||
var data = {"height": height, "width": width, "sizePreset": sizePreset};
|
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));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_size() {
|
function load_settings() {
|
||||||
var data = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
var data = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||||
if (data){
|
if (data){
|
||||||
$("#height-tag").val(data.height);
|
$("#height-tag").val(data.height);
|
||||||
$("#width-tag").val(data.width);
|
$("#width-tag").val(data.width);
|
||||||
$("#printerType").val(data.sizePreset);
|
$("#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);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
$("#printerType").val('brotherSmall');
|
$("#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_size();
|
||||||
|
change_check();
|
||||||
}
|
}
|
||||||
|
|
||||||
function change_size() {
|
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() {
|
function printpdf() {
|
||||||
var border = 2;
|
var border = 2;
|
||||||
|
var line = 5;
|
||||||
var height = parseInt($("#height-tag").val());
|
var height = parseInt($("#height-tag").val());
|
||||||
var width = parseInt($("#width-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;
|
max_tag_side = (Math.max(height, width)/2) + border;
|
||||||
if (max_tag_side < img_side) {
|
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;
|
min_tag_side = (Math.min(height, width)/2) + border;
|
||||||
var last_tag_code = '';
|
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]);
|
var pdf = new jsPDF('l', 'mm', [width, height]);
|
||||||
$(".tag").map(function(x, y) {
|
$(".tag").map(function(x, y) {
|
||||||
if (x != 0){
|
if (x != 0){
|
||||||
pdf.addPage();
|
pdf.addPage();
|
||||||
console.log(x)
|
|
||||||
};
|
};
|
||||||
|
var space = line + border;
|
||||||
|
if ($("#qrCheck").prop('checked')) {
|
||||||
|
space += img_side;
|
||||||
|
}
|
||||||
var tag = $(y).text();
|
var tag = $(y).text();
|
||||||
last_tag_code = tag;
|
last_tag_code = tag;
|
||||||
var imgData = $('#'+tag+' img').attr("src");
|
if ($("#qrCheck").prop('checked')) {
|
||||||
pdf.addImage(imgData, 'PNG', border, border, img_side, img_side);
|
var imgData = $('#'+tag+' img').attr("src");
|
||||||
pdf.text(tag, max_tag_side, min_tag_side);
|
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');
|
pdf.save('Tag_'+last_tag_code+'.pdf');
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
<!-- Template Main CSS File -->
|
<!-- Template Main CSS File -->
|
||||||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', filename='css/devicehub.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
<!-- =======================================================
|
<!-- =======================================================
|
||||||
* Template Name: NiceAdmin - v2.2.0
|
* Template Name: NiceAdmin - v2.2.0
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<a href="{{ url_for('inventory.devicelist')}}" class="logo d-flex align-items-center">
|
<a href="{{ url_for('inventory.devicelist')}}" class="logo d-flex align-items-center">
|
||||||
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
|
<img src="{{ url_for('static', filename='img/logo_usody_clock.png') }}" alt="">
|
||||||
</a>
|
</a>
|
||||||
<i class="bi bi-list toggle-sidebar-btn"></i>
|
<i class="bi bi-list toggle-sidebar-btn"></i>
|
||||||
</div><!-- End Logo -->
|
</div><!-- End Logo -->
|
||||||
|
@ -191,15 +191,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</li><!-- End Temporal Lots Nav -->
|
</li><!-- End Temporal Lots Nav -->
|
||||||
|
|
||||||
<li class="nav-heading">Utils</li>
|
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link collapsed" href="{{ url_for('labels.label_list')}}">
|
|
||||||
<i class="bi bi-tags"></i>
|
|
||||||
<span>Tags</span>
|
|
||||||
</a>
|
|
||||||
</li><!-- End Tags Page Nav -->
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</aside><!-- End Sidebar-->
|
</aside><!-- End Sidebar-->
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
<div class="d-flex justify-content-center py-4">
|
<div class="d-flex justify-content-center py-4">
|
||||||
<a href="{{ url_for('core.login') }}" class="logo d-flex align-items-center w-auto">
|
<a href="{{ url_for('core.login') }}" class="logo d-flex align-items-center w-auto">
|
||||||
<img src="{{ url_for('static', filename='img/usody-logo-black.svg') }}" alt="">
|
<img src="{{ url_for('static', filename='img/logo_usody_clock.png') }}" alt="">
|
||||||
</a>
|
</a>
|
||||||
</div><!-- End Logo -->
|
</div><!-- End Logo -->
|
||||||
|
|
||||||
|
|
|
@ -27,248 +27,40 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-8 d-none"><!-- TODO (hidden until is implemented )-->
|
<div class="col-xl-8">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body pt-3">
|
<div class="card-body pt-3">
|
||||||
<!-- Bordered Tabs -->
|
<!-- Bordered Tabs -->
|
||||||
<ul class="nav nav-tabs nav-tabs-bordered">
|
<ul class="nav nav-tabs nav-tabs-bordered">
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#profile-overview">Overview</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-edit">Edit Profile</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-settings">Settings</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-change-password">Change Password</button>
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-change-password">Change Password</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content pt-2">
|
<div class="tab-content pt-2">
|
||||||
|
|
||||||
<div class="tab-pane fade show active profile-overview" id="profile-overview">
|
<div class="tab-pane fade show active pt-3" id="profile-change-password">
|
||||||
<h5 class="card-title">About</h5>
|
|
||||||
<p class="small fst-italic">Sunt est soluta temporibus accusantium neque nam maiores cumque temporibus. Tempora libero non est unde veniam est qui dolor. Ut sunt iure rerum quae quisquam autem eveniet perspiciatis odit. Fuga sequi sed ea saepe at unde.</p>
|
|
||||||
|
|
||||||
<h5 class="card-title">Profile Details</h5>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label ">Full Name</div>
|
|
||||||
<div class="col-lg-9 col-md-8">Kevin Anderson</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Company</div>
|
|
||||||
<div class="col-lg-9 col-md-8">Lueilwitz, Wisoky and Leuschke</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Job</div>
|
|
||||||
<div class="col-lg-9 col-md-8">Web Designer</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Country</div>
|
|
||||||
<div class="col-lg-9 col-md-8">USA</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Address</div>
|
|
||||||
<div class="col-lg-9 col-md-8">A108 Adam Street, New York, NY 535022</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Phone</div>
|
|
||||||
<div class="col-lg-9 col-md-8">(436) 486-3538 x29071</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4 label">Email</div>
|
|
||||||
<div class="col-lg-9 col-md-8">k.anderson@example.com</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade profile-edit pt-3" id="profile-edit">
|
|
||||||
|
|
||||||
<!-- Profile Edit Form -->
|
|
||||||
<form>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="profileImage" class="col-md-4 col-lg-3 col-form-label">Profile Image</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<img src="{{ url_for('static', filename='img/profile-img.jpg') }}" alt="Profile">
|
|
||||||
<div class="pt-2">
|
|
||||||
<a href="#" class="btn btn-primary btn-sm" title="Upload new profile image"><i class="bi bi-upload"></i></a>
|
|
||||||
<a href="#" class="btn btn-danger btn-sm" title="Remove my profile image"><i class="bi bi-trash"></i></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="fullName" class="col-md-4 col-lg-3 col-form-label">Full Name</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="fullName" type="text" class="form-control" id="fullName" value="Kevin Anderson">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="about" class="col-md-4 col-lg-3 col-form-label">About</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<textarea name="about" class="form-control" id="about" style="height: 100px">Sunt est soluta temporibus accusantium neque nam maiores cumque temporibus. Tempora libero non est unde veniam est qui dolor. Ut sunt iure rerum quae quisquam autem eveniet perspiciatis odit. Fuga sequi sed ea saepe at unde.</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="company" class="col-md-4 col-lg-3 col-form-label">Company</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="company" type="text" class="form-control" id="company" value="Lueilwitz, Wisoky and Leuschke">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Job" class="col-md-4 col-lg-3 col-form-label">Job</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="job" type="text" class="form-control" id="Job" value="Web Designer">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Country" class="col-md-4 col-lg-3 col-form-label">Country</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="country" type="text" class="form-control" id="Country" value="USA">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Address" class="col-md-4 col-lg-3 col-form-label">Address</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="address" type="text" class="form-control" id="Address" value="A108 Adam Street, New York, NY 535022">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Phone" class="col-md-4 col-lg-3 col-form-label">Phone</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="phone" type="text" class="form-control" id="Phone" value="(436) 486-3538 x29071">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Email" class="col-md-4 col-lg-3 col-form-label">Email</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="email" type="email" class="form-control" id="Email" value="k.anderson@example.com">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Twitter" class="col-md-4 col-lg-3 col-form-label">Twitter Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="twitter" type="text" class="form-control" id="Twitter" value="https://twitter.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Facebook" class="col-md-4 col-lg-3 col-form-label">Facebook Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="facebook" type="text" class="form-control" id="Facebook" value="https://facebook.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Instagram" class="col-md-4 col-lg-3 col-form-label">Instagram Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="instagram" type="text" class="form-control" id="Instagram" value="https://instagram.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="Linkedin" class="col-md-4 col-lg-3 col-form-label">Linkedin Profile</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="linkedin" type="text" class="form-control" id="Linkedin" value="https://linkedin.com/#">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</form><!-- End Profile Edit Form -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade pt-3" id="profile-settings">
|
|
||||||
|
|
||||||
<!-- Settings Form -->
|
|
||||||
<form>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="fullName" class="col-md-4 col-lg-3 col-form-label">Email Notifications</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="changesMade" checked>
|
|
||||||
<label class="form-check-label" for="changesMade">
|
|
||||||
Changes made to your account
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="newProducts" checked>
|
|
||||||
<label class="form-check-label" for="newProducts">
|
|
||||||
Information on new products and services
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="proOffers">
|
|
||||||
<label class="form-check-label" for="proOffers">
|
|
||||||
Marketing and promo offers
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="securityNotify" checked disabled>
|
|
||||||
<label class="form-check-label" for="securityNotify">
|
|
||||||
Security alerts
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</form><!-- End settings Form -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane fade pt-3" id="profile-change-password">
|
|
||||||
<!-- Change Password Form -->
|
<!-- Change Password Form -->
|
||||||
<form>
|
<form action="{{ url_for('core.set-password') }}" method="post">
|
||||||
|
{% for f in password_form %}
|
||||||
|
{% if f == password_form.csrf_token %}
|
||||||
|
{{ f }}
|
||||||
|
{% else %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label for="currentPassword" class="col-md-4 col-lg-3 col-form-label">Current Password</label>
|
<label class="col-md-4 col-lg-3 col-form-label">{{ f.label }}</label>
|
||||||
<div class="col-md-8 col-lg-9">
|
<div class="col-md-8 col-lg-9">
|
||||||
<input name="password" type="password" class="form-control" id="currentPassword">
|
{{ f }}
|
||||||
|
{% if f.errors %}
|
||||||
|
<p class="text-danger">
|
||||||
|
{% for error in f.errors %}
|
||||||
|
{{ error }}<br/>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row mb-3">
|
{% endfor %}
|
||||||
<label for="newPassword" class="col-md-4 col-lg-3 col-form-label">New Password</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="newpassword" type="password" class="form-control" id="newPassword">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label for="renewPassword" class="col-md-4 col-lg-3 col-form-label">Re-enter New Password</label>
|
|
||||||
<div class="col-md-8 col-lg-9">
|
|
||||||
<input name="renewpassword" type="password" class="form-control" id="renewPassword">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button type="submit" class="btn btn-primary">Change Password</button>
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Adding to a tag</h5>
|
<h5 class="modal-title">Adding to a unique identifier</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="{{ url_for('inventory.tag_devices_add') }}" method="post">
|
<form action="{{ url_for('inventory.tag_devices_add') }}" method="post">
|
||||||
{{ form_tag_device.csrf_token }}
|
{{ form_tag_device.csrf_token }}
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
Please write a name of a tag
|
Please write a name of a unique identifier
|
||||||
<select class="form-control selectpicker" id="selectTag" name="tag" data-live-search="true">
|
<select class="form-control selectpicker" id="selectTag" name="tag" data-live-search="true">
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<option value="{{ tag.id }}">{{ tag.id }}</option>
|
<option value="{{ tag.id }}">{{ tag.id }}</option>
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
</select>
|
</select>
|
||||||
<input class="devicesList" type="hidden" name="device" />
|
<input class="devicesList" type="hidden" name="device" />
|
||||||
<p class="text-danger pol">
|
<p class="text-danger pol">
|
||||||
You need select first one device and only one for add this in a tag
|
You need select first one device and only one for add this in a unique identifier
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="confirmLotsModal" tabindex="-1" role="dialog" aria-labelledby="confirmLotsModal"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exampleModalLabel">Confirm lots changes</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
IF THIS TEXT APEARS THERE HAS AN ERROR
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" onclick="$('#confirmLotsModal').modal('hide');" class="btn btn-secondary"
|
||||||
|
style="margin-right: auto;" data-dismiss="#confirmLotsModal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="SaveAllActions">Save changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -26,6 +26,14 @@
|
||||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#type">Type</button>
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#type">Type</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ device.public_link }}" target="_blank">Web</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#status">Status</button>
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#status">Status</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -69,6 +77,50 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade profile-overview" id="lots">
|
||||||
|
<h5 class="card-title">Incoming Lots</h5>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for lot in device.lots %}
|
||||||
|
{% if lot.is_incoming %}
|
||||||
|
<div class="col">
|
||||||
|
<a class="ms-3" href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
|
||||||
|
<span>{{ lot.name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="card-title">Outgoing Lots</h5>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for lot in device.lots %}
|
||||||
|
{% if lot.is_outgoing %}
|
||||||
|
<div class="col">
|
||||||
|
<a class="ms-3" href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
|
||||||
|
<span>{{ lot.name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="card-title">Temporary Lots</h5>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for lot in device.lots %}
|
||||||
|
{% if lot.is_temporary %}
|
||||||
|
<div class="col">
|
||||||
|
<a class="ms-3" href="{{ url_for('inventory.lotdevicelist', lot_id=lot.id) }}">
|
||||||
|
<span>{{ lot.name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade profile-overview" id="status">
|
<div class="tab-pane fade profile-overview" id="status">
|
||||||
<h5 class="card-title">Status Details</h5>
|
<h5 class="card-title">Status Details</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
|
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
|
||||||
{% if not lot %}
|
{% if not lot %}
|
||||||
<li class="breadcrumb-item active">Unassgined</li>
|
<li class="breadcrumb-item active">Unassigned</li>
|
||||||
{% elif lot.is_temporary %}
|
{% elif lot.is_temporary %}
|
||||||
<li class="breadcrumb-item active">Temporary Lot</li>
|
<li class="breadcrumb-item active">Temporary Lot</li>
|
||||||
<li class="breadcrumb-item active">{{ lot.name }}</li>
|
<li class="breadcrumb-item active">{{ lot.name }}</li>
|
||||||
|
@ -32,21 +32,25 @@
|
||||||
<div class="card-body pt-3">
|
<div class="card-body pt-3">
|
||||||
<!-- Bordered Tabs -->
|
<!-- Bordered Tabs -->
|
||||||
|
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between row">
|
||||||
<h3><a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a></h3>
|
<h3 class="col-sm-12 col-md-5"><a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a></h3>
|
||||||
|
|
||||||
<div><!-- lot actions -->
|
<div class="col-sm-12 col-md-7 d-md-flex justify-content-md-end"><!-- lot actions -->
|
||||||
{% if lot.is_temporary %}
|
{% if lot.is_temporary %}
|
||||||
<span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
|
|
||||||
|
{% if 1 == 2 %}{# <!-- TODO (@slamora) Don't render Trade buttons until implementation is finished --> #}
|
||||||
<a class="me-2" href="javascript:newTrade('user_from')">
|
<a class="me-2" href="javascript:newTrade('user_from')">
|
||||||
<i class="bi bi-arrow-down-right"></i> Add supplier
|
<i class="bi bi-arrow-down-right"></i> Add supplier
|
||||||
</a>
|
</a>
|
||||||
<a class="me-2" href="javascript:newTrade('user_to')">
|
<a class="me-2" href="javascript:newTrade('user_to')">
|
||||||
<i class="bi bi-arrow-up-right"></i> Add receiver
|
<i class="bi bi-arrow-up-right"></i> Add receiver
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}{# <!-- /end TODO --> #}
|
||||||
|
|
||||||
<a class="text-danger" href="javascript:removeLot()">
|
<a class="text-danger" href="javascript:removeLot()">
|
||||||
<i class="bi bi-trash"></i> Delete Lot
|
<i class="bi bi-trash"></i> Delete Lot
|
||||||
</a>
|
</a>
|
||||||
|
<span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,7 +73,7 @@
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="tab-content pt-5">
|
<div class="tab-content pt-1">
|
||||||
<div id="devices-list" class="tab-pane fade devices-list active show">
|
<div id="devices-list" class="tab-pane fade devices-list active show">
|
||||||
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
|
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
|
||||||
<div class="btn-group dropdown ml-1">
|
<div class="btn-group dropdown ml-1">
|
||||||
|
@ -79,9 +83,9 @@
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span>
|
<span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span>
|
||||||
<ul class="dropdown-menu" aria-labelledby="btnLots" style="width: 300px;" id="dropDownLotsSelector">
|
<ul class="dropdown-menu" aria-labelledby="btnLots" id="dropDownLotsSelector">
|
||||||
<h6 class="dropdown-header">Select some devices to manage lots</h6>
|
<h6 class="dropdown-header">Select some devices to manage lots</h6>
|
||||||
<ul style="list-style-type: none; margin: 0; padding: 0;" class="mx-3" id="LotsSelector"></ul>
|
<ul class="mx-3" id="LotsSelector"></ul>
|
||||||
<li><hr /></li>
|
<li><hr /></li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#" class="dropdown-item" id="ApplyDeviceLots">
|
<a href="#" class="dropdown-item" id="ApplyDeviceLots">
|
||||||
|
@ -91,7 +95,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group dropdown ml-1" uib-dropdown="">
|
<div class="btn-group dropdown m-1" uib-dropdown="">
|
||||||
<button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-plus"></i>
|
<i class="bi bi-plus"></i>
|
||||||
New Actions
|
New Actions
|
||||||
|
@ -178,7 +182,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group dropdown ml-1" uib-dropdown="">
|
<div class="btn-group dropdown m-1" uib-dropdown="">
|
||||||
<button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-reply"></i>
|
<i class="bi bi-reply"></i>
|
||||||
Exports
|
Exports
|
||||||
|
@ -197,12 +201,6 @@
|
||||||
Metrics Spreadsheet
|
Metrics Spreadsheet
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="javascript:export_file('links')" class="dropdown-item">
|
|
||||||
<i class="bi bi-link-45deg"></i>
|
|
||||||
Public Links
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:export_file('certificates')" class="dropdown-item">
|
<a href="javascript:export_file('certificates')" class="dropdown-item">
|
||||||
<i class="bi bi-eraser-fill"></i>
|
<i class="bi bi-eraser-fill"></i>
|
||||||
|
@ -212,26 +210,43 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group dropdown ml-1" uib-dropdown="">
|
<div class="btn-group dropdown m-1" uib-dropdown="">
|
||||||
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button id="btnUniqueID" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-tag"></i>
|
<i class="bi bi-tag"></i>
|
||||||
Tags
|
Unique Identifiers
|
||||||
</button>
|
</button>
|
||||||
<span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span>
|
<span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span>
|
||||||
<span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span>
|
<span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span>
|
||||||
<ul class="dropdown-menu" aria-labelledby="btnTags">
|
<ul class="dropdown-menu" aria-labelledby="btnUniqueID">
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:addTag()" class="dropdown-item">
|
<a href="javascript:addTag()" class="dropdown-item">
|
||||||
<i class="bi bi-plus"></i>
|
<i class="bi bi-plus"></i>
|
||||||
Add Tag to selected Device
|
Add Unique Identifier to selected Device
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:removeTag()" class="dropdown-item">
|
<a href="javascript:removeTag()" class="dropdown-item">
|
||||||
<i class="bi bi-x"></i>
|
<i class="bi bi-x"></i>
|
||||||
Remove Tag from selected Device
|
Remove Unique Identifier from selected Device
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ url_for('labels.label_list')}}">
|
||||||
|
<i class="bi bi-tools"></i>
|
||||||
|
Unique Identifier Management
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group dropdown m-1" uib-dropdown="">
|
||||||
|
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="bi bi-tag"></i>
|
||||||
|
Labels
|
||||||
|
</button>
|
||||||
|
<span class="d-none" id="unlinkTagAlertModal" data-bs-toggle="modal" data-bs-target="#unlinkTagErrorModal"></span>
|
||||||
|
<span class="d-none" id="addTagAlertModal" data-bs-toggle="modal" data-bs-target="#addingTagModal"></span>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="btnTags">
|
||||||
<li>
|
<li>
|
||||||
<form id="print_labels" method="post" action="{{ url_for('labels.print_labels') }}">
|
<form id="print_labels" method="post" action="{{ url_for('labels.print_labels') }}">
|
||||||
{% for f in form_print_labels %}
|
{% for f in form_print_labels %}
|
||||||
|
@ -246,7 +261,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group dropdown ml-1" uib-dropdown="">
|
<div class="btn-group dropdown m-1" uib-dropdown="">
|
||||||
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="bi bi-laptop"></i>
|
<i class="bi bi-laptop"></i>
|
||||||
New Device
|
New Device
|
||||||
|
@ -293,6 +308,10 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="select-devices-info" class="alert alert-info mb-0 mt-3 d-none" role="alert">
|
||||||
|
If this text is showing is because there are an error
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-content pt-2">
|
<div class="tab-content pt-2">
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<div class="d-flex mt-4 mb-4">
|
<div class="d-flex mt-4 mb-4">
|
||||||
|
@ -314,7 +333,7 @@
|
||||||
<th scope="col">Select</th>
|
<th scope="col">Select</th>
|
||||||
<th scope="col">Title</th>
|
<th scope="col">Title</th>
|
||||||
<th scope="col">DHID</th>
|
<th scope="col">DHID</th>
|
||||||
<th scope="col">Tags</th>
|
<th scope="col">Unique Identifiers</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">Status</th>
|
||||||
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Update</th>
|
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Update</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -325,13 +344,16 @@
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="deviceSelect" data="{{ dev.id }}"
|
<input type="checkbox" class="deviceSelect" data="{{ dev.id }}"
|
||||||
data-device-type="{{ dev.type }}" data-device-manufacturer="{{ dev.manufacturer }}"
|
data-device-type="{{ dev.type }}" data-device-manufacturer="{{ dev.manufacturer }}"
|
||||||
data-device-dhid="{{ dev.devicehub_id }}"
|
data-device-dhid="{{ dev.devicehub_id }}" data-device-vname="{{ dev.verbose_name }}"
|
||||||
{% if form_new_allocate.type.data and dev.id in list_devices %}
|
{% if form_new_allocate.type.data and dev.id in list_devices %}
|
||||||
checked="checked"
|
checked="checked"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if dev.get_type_logo() %}
|
||||||
|
<i class="{{ dev.get_type_logo() }}" title="{{ dev.type }}"></i>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
|
<a href="{{ url_for('inventory.device_details', id=dev.devicehub_id)}}">
|
||||||
{{ dev.verbose_name }}
|
{{ dev.verbose_name }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -347,7 +369,7 @@
|
||||||
{% if not loop.last %},{% endif %}
|
{% if not loop.last %},{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{% if dev.status %}{{ dev.status }}{% endif %}</td>
|
<td>{% if dev.status %}{{ dev.status.type }}{% endif %}</td>
|
||||||
<td>{{ dev.updated.strftime('%H:%M %d-%m-%Y') }}</td>
|
<td>{{ dev.updated.strftime('%H:%M %d-%m-%Y') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -404,10 +426,13 @@
|
||||||
{% include "inventory/trade.html" %}
|
{% include "inventory/trade.html" %}
|
||||||
{% include "inventory/alert_export_error.html" %}
|
{% include "inventory/alert_export_error.html" %}
|
||||||
{% include "inventory/alert_unlink_tag_error.html" %}
|
{% include "inventory/alert_unlink_tag_error.html" %}
|
||||||
|
{% include "inventory/alert_lots_changes.html" %}
|
||||||
|
|
||||||
<!-- Custom Code -->
|
<!-- Custom Code -->
|
||||||
<script>
|
<script>
|
||||||
const table = new simpleDatatables.DataTable("table")
|
const table = new simpleDatatables.DataTable("table", {
|
||||||
|
perPage: 20
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="pt-4 pb-2">
|
<div class="pt-4 pb-2">
|
||||||
<h1 class="card-title text-center pb-0 fs-4">Unlink Tag from Device</h1>
|
<h1 class="card-title text-center pb-0 fs-4">Unlink Unique Identifier from Device</h1>
|
||||||
<p class="text-center small">Please enter a code for the tag.</p>
|
<p class="text-center small">Please enter a code for the unique identifier.</p>
|
||||||
{% if form.form_errors %}
|
{% if form.form_errors %}
|
||||||
<p class="text-danger">
|
<p class="text-danger">
|
||||||
{% for error in form.form_errors %}
|
{% for error in form.form_errors %}
|
||||||
|
@ -33,10 +33,10 @@
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="tag" class="form-label">Tag</label>
|
<label for="tag" class="form-label">Unique Identifier</label>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
{{ form.tag(class_="form-control") }}
|
{{ form.tag(class_="form-control") }}
|
||||||
<div class="invalid-feedback">Please select tag.</div>
|
<div class="invalid-feedback">Please select unique identifier.</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.tag.errors %}
|
{% if form.tag.errors %}
|
||||||
<p class="text-danger">
|
<p class="text-danger">
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
<h1>Inventory</h1>
|
<h1>Inventory</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
|
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
|
||||||
<li class="breadcrumb-item active">Tag details {{ tag.id }}</li>
|
<li class="breadcrumb-item active">Unique Identifier details {{ tag.id }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div><!-- End Page Title -->
|
</div><!-- End Page Title -->
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3 col-md-4 label ">Type</div>
|
<div class="col-lg-3 col-md-4 label ">Type</div>
|
||||||
<div class="col-lg-9 col-md-8">{% if tag.provider %}UnNamed Tag{% else %}Named{% endif %}</div>
|
<div class="col-lg-9 col-md-8">{% if tag.provider %}UnNamed Unique Identifier{% else %}Named Unique Identifier{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -43,16 +43,49 @@
|
||||||
<h5 class="card-title">Print Label</h5>
|
<h5 class="card-title">Print Label</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3 col-md-4">
|
<div class="col-lg-3 col-md-4">
|
||||||
<div style="width:256px; height:148px; border: solid 1px; padding: 10px;">
|
<div style="width:256px; min-height:148px; border: solid 1px; padding: 10px;">
|
||||||
<div id="print">
|
<div id="print">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col qr">
|
||||||
<div id="{{ tag.id }}"></div>
|
<div id="{{ tag.id }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col dhid">
|
||||||
<div style="padding-top: 55px"><b class="tag">{{ tag.id }}</b></div>
|
<div style="padding-top: 55px">
|
||||||
|
{% if tag.device %}
|
||||||
|
<b class="tag" data-serial-number="{{ tag.device.serial_number or '' }}"
|
||||||
|
data-manufacturer="{{ tag.device.manufacturer or '' }}"
|
||||||
|
data-model="{{ tag.device.model or '' }}">{{ tag.id }}</b>
|
||||||
|
{% else %}
|
||||||
|
<b class="tag" data-serial-number=""
|
||||||
|
data-manufacturer=""
|
||||||
|
data-model="">{{ tag.id }}</b>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if tag.device %}
|
||||||
|
<div class="row serial_number" style="display: none">
|
||||||
|
<div class="col">
|
||||||
|
<div>
|
||||||
|
<b>{{ tag.device.serial_number or '' }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row manufacturer" style="display: none">
|
||||||
|
<div class="col">
|
||||||
|
<div>
|
||||||
|
<b>{{ tag.device.manufacturer or '' }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row model" style="display: none">
|
||||||
|
<div class="col">
|
||||||
|
<div>
|
||||||
|
<span style="font-size: 12px;">{{ tag.device.model or '' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,20 +117,43 @@
|
||||||
<span class="input-group-text">mm</span>
|
<span class="input-group-text">mm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if tag.device %}
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="qr" type="checkbox" id="qrCheck" checked="">
|
||||||
|
<label class="form-check-label" for="qrCheck">QR</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="dhid" type="checkbox" id="dhidCheck" checked="">
|
||||||
|
<label class="form-check-label" for="dhidCheck">Unique Identifier</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="serial_number" type="checkbox" id="serialNumberCheck">
|
||||||
|
<label class="form-check-label" for="serialNumberCheck">Serial number</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="manufacturer" type="checkbox" id="manufacturerCheck">
|
||||||
|
<label class="form-check-label" for="manufacturerCheck">Manufacturer</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="model" type="checkbox" id="modelCheck">
|
||||||
|
<label class="form-check-label" for="modelCheck">Model</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-lg-3 col-md-4">
|
||||||
|
<a href="javascript:printpdf()" class="btn btn-success">Print labels</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3 col-md-4">
|
||||||
|
<a href="javascript:save_settings()" class="btn btn-primary">Save settings</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3 col-md-4">
|
||||||
|
<a href="javascript:reset_settings()" class="btn btn-danger">Reset settings</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-3 col-md-4">
|
|
||||||
<a href="javascript:printpdf()" class="btn btn-success">Print</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-4">
|
|
||||||
<a href="javascript:save_size()" class="btn btn-primary">Save</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-4">
|
|
||||||
<a href="javascript:reset_size()" class="btn btn-danger">Reset</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,18 +19,18 @@
|
||||||
<div class="card-body pt-3">
|
<div class="card-body pt-3">
|
||||||
<!-- Bordered Tabs -->
|
<!-- Bordered Tabs -->
|
||||||
|
|
||||||
<div class="btn-group dropdown ml-1">
|
<div class="btn-group dropdown m-1">
|
||||||
<a href="{{ url_for('labels.tag_add')}}" type="button" class="btn btn-primary">
|
<a href="{{ url_for('labels.tag_add')}}" type="button" class="btn btn-primary">
|
||||||
<i class="bi bi-plus"></i>
|
<i class="bi bi-plus"></i>
|
||||||
Create Named Tag
|
Create Named Unique Identifier
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group dropdown ml-1" uib-dropdown="">
|
<div class="btn-group dropdown m-1" uib-dropdown="">
|
||||||
<a href="{{ url_for('labels.tag_unnamed_add')}}" type="button" class="btn btn-primary">
|
<a href="{{ url_for('labels.tag_unnamed_add')}}" type="button" class="btn btn-primary">
|
||||||
<i class="bi bi-plus"></i>
|
<i class="bi bi-plus"></i>
|
||||||
Create UnNamed Tag
|
Create UnNamed Unique Identifier
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,11 +53,11 @@
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for('labels.label_details', id=tag.id) }}">{{ tag.id }}</a></td>
|
<td><a href="{{ url_for('labels.label_details', id=tag.id) }}">{{ tag.id }}</a></td>
|
||||||
<td>{% if tag.provider %}Unnamed tag {% else %}Named tag{% endif %}</td>
|
<td>{% if tag.provider %}Unnamed unique Identifier {% else %}Named unique identifier{% endif %}</td>
|
||||||
<td>{{ tag.get_provider }}</td>
|
<td>{{ tag.get_provider }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if tag.device %}
|
{% if tag.device %}
|
||||||
<a href={{ url_for('inventory.device_details', id=tag.device.devicehub_id)}}>
|
<a href="{{ url_for('inventory.device_details', id=tag.device.devicehub_id)}}">
|
||||||
{{ tag.device.verbose_name }}
|
{{ tag.device.verbose_name }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<h1>Print Labels</h1>
|
<h1>Print Labels</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
|
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
|
||||||
<li class="breadcrumb-item active">Print Labels</li>
|
<li class="breadcrumb-item active">Print Labels</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -24,16 +24,39 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-3 col-md-4">
|
<div class="col-lg-3 col-md-4">
|
||||||
{% for tag in tags %}
|
{% for dev in devices %}
|
||||||
<div style="width:256px; height:148px; border: solid 1px; padding: 10px;">
|
<div style="width:256px; min-height:148px; border: solid 1px; padding: 10px;">
|
||||||
<div id="print">
|
<div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col qr">
|
||||||
<div id="{{ tag.id }}"></div>
|
<div id="{{ dev.devicehub_id }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col dhid">
|
||||||
<div style="padding-top: 55px">
|
<div style="padding-top: 55px">
|
||||||
<b class="tag">{{ tag.id }}</b>
|
<b class="tag" data-serial-number="{{ dev.serial_number or '' }}"
|
||||||
|
data-manufacturer="{{ dev.manufacturer or '' }}"
|
||||||
|
data-model="{{ dev.model or '' }}">{{ dev.devicehub_id }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row serial_number" style="display: none">
|
||||||
|
<div class="col">
|
||||||
|
<div>
|
||||||
|
<b>{{ dev.serial_number or '' }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row manufacturer" style="display: none">
|
||||||
|
<div class="col">
|
||||||
|
<div>
|
||||||
|
<b>{{ dev.manufacturer or '' }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row model" style="display: none">
|
||||||
|
<div class="col">
|
||||||
|
<div>
|
||||||
|
<span style="font-size: 12px;">{{ dev.model or '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,20 +94,41 @@
|
||||||
<span class="input-group-text">mm</span>
|
<span class="input-group-text">mm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="qr" type="checkbox" id="qrCheck" checked="">
|
||||||
|
<label class="form-check-label" for="qrCheck">QR</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="dhid" type="checkbox" id="dhidCheck" checked="">
|
||||||
|
<label class="form-check-label" for="dhidCheck">Dhid</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="serial_number" type="checkbox" id="serialNumberCheck">
|
||||||
|
<label class="form-check-label" for="serialNumberCheck">Serial number</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="manufacturer" type="checkbox" id="manufacturerCheck">
|
||||||
|
<label class="form-check-label" for="manufacturerCheck">Manufacturer</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-switch">
|
||||||
|
<input class="form-check-input" name="model" type="checkbox" id="modelCheck">
|
||||||
|
<label class="form-check-label" for="modelCheck">Model</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-lg-3 col-md-4">
|
||||||
|
<a href="javascript:printpdf()" class="btn btn-success">Print labels</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3 col-md-4">
|
||||||
|
<a href="javascript:save_settings()" class="btn btn-primary">Save settings</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3 col-md-4">
|
||||||
|
<a href="javascript:reset_settings()" class="btn btn-danger">Reset settings</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-5">
|
|
||||||
<div class="col-lg-3 col-md-4">
|
|
||||||
<a href="javascript:printpdf()" class="btn btn-success">Print</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-4">
|
|
||||||
<a href="javascript:save_size()" class="btn btn-primary">Save</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-4">
|
|
||||||
<a href="javascript:reset_size()" class="btn btn-danger">Reset</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -96,8 +140,8 @@
|
||||||
<script src="{{ url_for('static', filename='js/jspdf.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/jspdf.min.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% for tag in tags %}
|
{% for dev in devices %}
|
||||||
qr_draw("{{ url_for('inventory.device_details', id=tag.device.devicehub_id, _external=True) }}", "#{{ tag.id }}")
|
qr_draw("{{ url_for('inventory.device_details', id=dev.devicehub_id, _external=True) }}", "#{{ dev.devicehub_id }}")
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
|
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
|
||||||
<li class="breadcrumb-item">{{ page_title }}</li>
|
<li class="breadcrumb-item">{{ page_title }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -19,8 +19,8 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="pt-4 pb-2">
|
<div class="pt-4 pb-2">
|
||||||
<h5 class="card-title text-center pb-0 fs-4">Add a new Tag</h5>
|
<h5 class="card-title text-center pb-0 fs-4">Add a new Unique Identifier</h5>
|
||||||
<p class="text-center small">Please enter a code for the tag.</p>
|
<p class="text-center small">Please enter a code for the unique identifier.</p>
|
||||||
{% if form.form_errors %}
|
{% if form.form_errors %}
|
||||||
<p class="text-danger">
|
<p class="text-danger">
|
||||||
{% for error in form.form_errors %}
|
{% for error in form.form_errors %}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<label for="code" class="form-label">code</label>
|
<label for="code" class="form-label">code</label>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
<input type="text" name="code" class="form-control" required value="{{ form.code.data|default('', true) }}">
|
<input type="text" name="code" class="form-control" required value="{{ form.code.data|default('', true) }}">
|
||||||
<div class="invalid-feedback">Please enter a code of the tag.</div>
|
<div class="invalid-feedback">Please enter a code of the unique identifier.</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.code.errors %}
|
{% if form.code.errors %}
|
||||||
<p class="text-danger">
|
<p class="text-danger">
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
|
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
|
||||||
<li class="breadcrumb-item">{{ page_title }}</li>
|
<li class="breadcrumb-item">{{ page_title }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -19,8 +19,8 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="pt-4 pb-2">
|
<div class="pt-4 pb-2">
|
||||||
<h5 class="card-title text-center pb-0 fs-4">Add new Unnamed Tags</h5>
|
<h5 class="card-title text-center pb-0 fs-4">Add new Unnamed Unique Identifiers</h5>
|
||||||
<p class="text-center small">Please enter a number of the tags to issue.</p>
|
<p class="text-center small">Please enter a number of the unique identifiers to issue.</p>
|
||||||
{% if form.form_errors %}
|
{% if form.form_errors %}
|
||||||
<p class="text-danger">
|
<p class="text-danger">
|
||||||
{% for error in form.form_errors %}
|
{% for error in form.form_errors %}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<label for="code" class="form-label">Amount</label>
|
<label for="code" class="form-label">Amount</label>
|
||||||
<div class="input-group has-validation">
|
<div class="input-group has-validation">
|
||||||
{{ form.amount(class_="form-control") }}
|
{{ form.amount(class_="form-control") }}
|
||||||
<div class="invalid-feedback">Please enter a number of the tags to issue.</div>
|
<div class="invalid-feedback">Please enter a number of the unique identifiers to issue.</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.amount.errors %}
|
{% if form.amount.errors %}
|
||||||
<p class="text-danger">
|
<p class="text-danger">
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import flask
|
import flask
|
||||||
from flask import Blueprint
|
from flask import Blueprint, g
|
||||||
from flask.views import View
|
from flask.views import View
|
||||||
from flask_login import current_user, login_required, login_user, logout_user
|
from flask_login import current_user, login_required, login_user, logout_user
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
from ereuse_devicehub import __version__
|
from ereuse_devicehub import __version__, messages
|
||||||
from ereuse_devicehub.forms import LoginForm
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.forms import LoginForm, PasswordForm
|
||||||
|
from ereuse_devicehub.resources.action.models import Trade
|
||||||
|
from ereuse_devicehub.resources.lot.models import Lot
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
from ereuse_devicehub.utils import is_safe_url
|
from ereuse_devicehub.utils import is_safe_url
|
||||||
|
|
||||||
|
@ -45,18 +49,65 @@ class LogoutView(View):
|
||||||
return flask.redirect(flask.url_for('core.login'))
|
return flask.redirect(flask.url_for('core.login'))
|
||||||
|
|
||||||
|
|
||||||
class UserProfileView(View):
|
class GenericMixView(View):
|
||||||
|
decorators = [login_required]
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
self.context = {
|
||||||
|
'lots': self.get_lots(),
|
||||||
|
'version': __version__,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.context
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileView(GenericMixView):
|
||||||
decorators = [login_required]
|
decorators = [login_required]
|
||||||
template_name = 'ereuse_devicehub/user_profile.html'
|
template_name = 'ereuse_devicehub/user_profile.html'
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
context = {
|
self.get_context()
|
||||||
'current_user': current_user,
|
self.context.update(
|
||||||
'version': __version__,
|
{
|
||||||
}
|
'current_user': current_user,
|
||||||
return flask.render_template(self.template_name, **context)
|
'password_form': PasswordForm(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return flask.render_template(self.template_name, **self.context)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPasswordView(View):
|
||||||
|
methods = ['POST']
|
||||||
|
decorators = [login_required]
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
form = PasswordForm()
|
||||||
|
db.session.commit()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
form.save(commit=False)
|
||||||
|
messages.success('Reset user password successfully!')
|
||||||
|
else:
|
||||||
|
messages.error('Error modifying user password!')
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return flask.redirect(flask.url_for('core.user-profile'))
|
||||||
|
|
||||||
|
|
||||||
core.add_url_rule('/login/', view_func=LoginView.as_view('login'))
|
core.add_url_rule('/login/', view_func=LoginView.as_view('login'))
|
||||||
core.add_url_rule('/logout/', view_func=LogoutView.as_view('logout'))
|
core.add_url_rule('/logout/', view_func=LogoutView.as_view('logout'))
|
||||||
core.add_url_rule('/profile/', view_func=UserProfileView.as_view('user-profile'))
|
core.add_url_rule('/profile/', view_func=UserProfileView.as_view('user-profile'))
|
||||||
|
core.add_url_rule('/set_password/', view_func=UserPasswordView.as_view('set-password'))
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "workspace",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Devicehub is a distributed IT Asset Management System focused in reusing devices, created under the project [eReuse.org](https://www.ereuse.org)",
|
||||||
|
"main": "index.js",
|
||||||
|
"directories": {
|
||||||
|
"doc": "docs",
|
||||||
|
"example": "examples",
|
||||||
|
"test": "tests"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.13.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint:report": "eslint ereuse_devicehub --ext .js --output-file eslint_report.json --format json",
|
||||||
|
"lint:fix": "eslint ereuse_devicehub --ext .js --fix"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
|
"eslint-plugin-react": "^7.29.4",
|
||||||
|
"eslint-plugin-react-hooks": "^4.4.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,31 @@
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import jwt
|
|
||||||
import ereuse_utils
|
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from decouple import config
|
|
||||||
|
|
||||||
import boltons.urlutils
|
import boltons.urlutils
|
||||||
|
import ereuse_utils
|
||||||
|
import jwt
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
from decouple import config
|
||||||
from psycopg2 import IntegrityError
|
from psycopg2 import IntegrityError
|
||||||
from sqlalchemy.exc import ProgrammingError
|
from sqlalchemy.exc import ProgrammingError
|
||||||
|
|
||||||
from ereuse_devicehub.client import Client, UserClient
|
from ereuse_devicehub.api.views import api
|
||||||
|
from ereuse_devicehub.client import Client, UserClient, UserClientFlask
|
||||||
from ereuse_devicehub.config import DevicehubConfig
|
from ereuse_devicehub.config import DevicehubConfig
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.devicehub import Devicehub
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.inventory.views import devices
|
||||||
|
from ereuse_devicehub.labels.views import labels
|
||||||
from ereuse_devicehub.resources.agent.models import Person
|
from ereuse_devicehub.resources.agent.models import Person
|
||||||
from ereuse_devicehub.resources.tag import Tag
|
|
||||||
from ereuse_devicehub.resources.user.models import User
|
|
||||||
from ereuse_devicehub.resources.user.models import Session
|
|
||||||
from ereuse_devicehub.resources.enums import SessionType
|
from ereuse_devicehub.resources.enums import SessionType
|
||||||
from ereuse_devicehub.api.views import api
|
from ereuse_devicehub.resources.tag import Tag
|
||||||
|
from ereuse_devicehub.resources.user.models import Session, User
|
||||||
|
from ereuse_devicehub.views import core
|
||||||
|
|
||||||
STARTT = datetime(year=2000, month=1, day=1, hour=1)
|
STARTT = datetime(year=2000, month=1, day=1, hour=1)
|
||||||
"""A dummy starting time to use in tests."""
|
"""A dummy starting time to use in tests."""
|
||||||
|
@ -52,6 +54,21 @@ def config():
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def _app(config: TestConfig) -> Devicehub:
|
def _app(config: TestConfig) -> Devicehub:
|
||||||
|
# dh_config = DevicehubConfig()
|
||||||
|
# config = TestConfig(dh_config)
|
||||||
|
app = Devicehub(inventory='test', config=config, db=db)
|
||||||
|
app.register_blueprint(core)
|
||||||
|
app.register_blueprint(devices)
|
||||||
|
app.register_blueprint(labels)
|
||||||
|
app.register_blueprint(api)
|
||||||
|
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
|
||||||
|
app.config['PROFILE'] = True
|
||||||
|
# app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def _app2(config: TestConfig) -> Devicehub:
|
||||||
return Devicehub(inventory='test', config=config, db=db)
|
return Devicehub(inventory='test', config=config, db=db)
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,14 +80,15 @@ def app(request, _app: Devicehub) -> Devicehub:
|
||||||
db.drop_all()
|
db.drop_all()
|
||||||
|
|
||||||
def _init():
|
def _init():
|
||||||
_app.init_db(name='Test Inventory',
|
_app.init_db(
|
||||||
org_name='FooOrg',
|
name='Test Inventory',
|
||||||
org_id='foo-org-id',
|
org_name='FooOrg',
|
||||||
tag_url=boltons.urlutils.URL('https://example.com'),
|
org_id='foo-org-id',
|
||||||
tag_token=uuid.UUID('52dacef0-6bcb-4919-bfed-f10d2c96ecee'),
|
tag_url=boltons.urlutils.URL('https://example.com'),
|
||||||
erase=False,
|
tag_token=uuid.UUID('52dacef0-6bcb-4919-bfed-f10d2c96ecee'),
|
||||||
common=True)
|
erase=False,
|
||||||
_app.register_blueprint(api)
|
common=True,
|
||||||
|
)
|
||||||
|
|
||||||
with _app.app_context():
|
with _app.app_context():
|
||||||
try:
|
try:
|
||||||
|
@ -102,7 +120,9 @@ def user(app: Devicehub) -> UserClient:
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
password = 'foo'
|
password = 'foo'
|
||||||
user = create_user(password=password)
|
user = create_user(password=password)
|
||||||
client = UserClient(app, user.email, password, response_wrapper=app.response_class)
|
client = UserClient(
|
||||||
|
app, user.email, password, response_wrapper=app.response_class
|
||||||
|
)
|
||||||
client.login()
|
client.login()
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
@ -114,11 +134,34 @@ def user2(app: Devicehub) -> UserClient:
|
||||||
password = 'foo'
|
password = 'foo'
|
||||||
email = 'foo2@foo.com'
|
email = 'foo2@foo.com'
|
||||||
user = create_user(email=email, password=password)
|
user = create_user(email=email, password=password)
|
||||||
client = UserClient(app, user.email, password, response_wrapper=app.response_class)
|
client = UserClient(
|
||||||
|
app, user.email, password, response_wrapper=app.response_class
|
||||||
|
)
|
||||||
client.login()
|
client.login()
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def user3(app: Devicehub) -> UserClientFlask:
|
||||||
|
"""Gets a client with a logged-in dummy user."""
|
||||||
|
with app.app_context():
|
||||||
|
password = 'foo'
|
||||||
|
user = create_user(password=password)
|
||||||
|
client = UserClientFlask(app, user.email, password)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def user4(app: Devicehub) -> UserClient:
|
||||||
|
"""Gets a client with a logged-in dummy user."""
|
||||||
|
with app.app_context():
|
||||||
|
password = 'foo'
|
||||||
|
email = 'foo2@foo.com'
|
||||||
|
user = create_user(email=email, password=password)
|
||||||
|
client = UserClientFlask(app, user.email, password)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
def create_user(email='foo@foo.com', password='foo') -> User:
|
def create_user(email='foo@foo.com', password='foo') -> User:
|
||||||
user = User(email=email, password=password)
|
user = User(email=email, password=password)
|
||||||
user.individuals.add(Person(name='Timmy'))
|
user.individuals.add(Person(name='Timmy'))
|
||||||
|
@ -148,16 +191,13 @@ def auth_app_context(app: Devicehub):
|
||||||
def json_encode(dev: str) -> dict:
|
def json_encode(dev: str) -> dict:
|
||||||
"""Encode json."""
|
"""Encode json."""
|
||||||
data = {"type": "Snapshot"}
|
data = {"type": "Snapshot"}
|
||||||
data['data'] = jwt.encode(dev,
|
data['data'] = jwt.encode(
|
||||||
P,
|
dev, P, algorithm="HS256", json_encoder=ereuse_utils.JSONEncoder
|
||||||
algorithm="HS256",
|
|
||||||
json_encoder=ereuse_utils.JSONEncoder
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def yaml2json(name: str) -> dict:
|
def yaml2json(name: str) -> dict:
|
||||||
"""Opens and parses a YAML file from the ``files`` subdir."""
|
"""Opens and parses a YAML file from the ``files`` subdir."""
|
||||||
with Path(__file__).parent.joinpath('files').joinpath(name + '.yaml').open() as f:
|
with Path(__file__).parent.joinpath('files').joinpath(name + '.yaml').open() as f:
|
||||||
|
@ -176,7 +216,9 @@ def file_json(name):
|
||||||
|
|
||||||
def file_workbench(name: str) -> dict:
|
def file_workbench(name: str) -> dict:
|
||||||
"""Opens and parses a YAML file from the ``files`` subdir."""
|
"""Opens and parses a YAML file from the ``files`` subdir."""
|
||||||
with Path(__file__).parent.joinpath('workbench_files').joinpath(name + '.json').open() as f:
|
with Path(__file__).parent.joinpath('workbench_files').joinpath(
|
||||||
|
name + '.json'
|
||||||
|
).open() as f:
|
||||||
return yaml.load(f)
|
return yaml.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"timestamp": "2022-03-31T19:09:57.167164",
|
"timestamp": "2022-03-31T19:09:57.167164",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"uuid": "cdecaf47-6e32-4ccb-b689-95c064d8c514",
|
"uuid": "cdecaf47-6e32-4ccb-b689-95c064d8c514",
|
||||||
"wbid": "MLKO1Y0R55XZM051WQ5KJM01RY44Q",
|
"sid": "MLKO1Y0R55XZM051WQ5KJM01RY44Q",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"version": "2022.03.00",
|
"version": "2022.03.00",
|
||||||
"schema_api": "1.0.0",
|
"schema_api": "1.0.0",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"timestamp": "2022-04-01 06:28:54.099394",
|
"timestamp": "2022-04-01 06:28:54.099394",
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"uuid": "232b44f3-b139-490e-90c8-2748a4523e80",
|
"uuid": "232b44f3-b139-490e-90c8-2748a4523e80",
|
||||||
"wbid": "YKPZ27NJ2NMRO4893M4L5NRZV5YJ1",
|
"sid": "YKPZ27NJ2NMRO4893M4L5NRZV5YJ1",
|
||||||
"software": "Workbench",
|
"software": "Workbench",
|
||||||
"version": "2022.03.00",
|
"version": "2022.03.00",
|
||||||
"schema_api": "1.0.0",
|
"schema_api": "1.0.0",
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -3375,5 +3375,5 @@
|
||||||
"type": "Snapshot",
|
"type": "Snapshot",
|
||||||
"uuid": "b7438448-7852-42c6-ad22-0963cf7c5b43",
|
"uuid": "b7438448-7852-42c6-ad22-0963cf7c5b43",
|
||||||
"version": "2022.03.3-alpha",
|
"version": "2022.03.3-alpha",
|
||||||
"wbid": "LYWP7"
|
"sid": "LYWP7"
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ereuse_devicehub.devicehub import Devicehub
|
|
||||||
from ereuse_devicehub.client import Client
|
from ereuse_devicehub.client import Client
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.mvp
|
@pytest.mark.mvp
|
||||||
|
@ -28,38 +28,66 @@ def test_api_docs(client: Client):
|
||||||
"""Tests /apidocs correct initialization."""
|
"""Tests /apidocs correct initialization."""
|
||||||
docs, _ = client.get('/apidocs')
|
docs, _ = client.get('/apidocs')
|
||||||
assert set(docs['paths'].keys()) == {
|
assert set(docs['paths'].keys()) == {
|
||||||
|
'/',
|
||||||
'/actions/',
|
'/actions/',
|
||||||
|
'/allocates/',
|
||||||
'/apidocs',
|
'/apidocs',
|
||||||
'/api/inventory/',
|
'/api/inventory/',
|
||||||
'/allocates/',
|
|
||||||
'/deallocates/',
|
'/deallocates/',
|
||||||
'/deliverynotes/',
|
'/deliverynotes/',
|
||||||
'/devices/',
|
'/devices/',
|
||||||
'/devices/static/{filename}',
|
'/devices/static/{filename}',
|
||||||
'/documents/static/{filename}',
|
|
||||||
'/documents/actions/',
|
'/documents/actions/',
|
||||||
'/documents/erasures/',
|
|
||||||
'/documents/devices/',
|
|
||||||
'/documents/stamps/',
|
|
||||||
'/documents/wbconf/{wbtype}',
|
|
||||||
'/documents/internalstats/',
|
|
||||||
'/documents/stock/',
|
|
||||||
'/documents/check/',
|
'/documents/check/',
|
||||||
|
'/documents/devices/',
|
||||||
|
'/documents/erasures/',
|
||||||
|
'/documents/internalstats/',
|
||||||
'/documents/lots/',
|
'/documents/lots/',
|
||||||
'/versions/',
|
'/documents/stamps/',
|
||||||
'/manufacturers/',
|
'/documents/static/{filename}',
|
||||||
|
'/documents/stock/',
|
||||||
|
'/documents/wbconf/{wbtype}',
|
||||||
|
'/inventory/action/add/',
|
||||||
|
'/inventory/action/allocate/add/',
|
||||||
|
'/inventory/action/datawipe/add/',
|
||||||
|
'/inventory/action/trade/add/',
|
||||||
|
'/inventory/device/',
|
||||||
|
'/inventory/device/add/',
|
||||||
|
'/inventory/device/{id}/',
|
||||||
|
'/inventory/export/{export_id}/',
|
||||||
|
'/inventory/lot/add/',
|
||||||
|
'/inventory/lot/{id}/',
|
||||||
|
'/inventory/lot/{id}/del/',
|
||||||
|
'/inventory/lot/{lot_id}/device/',
|
||||||
|
'/inventory/lot/{lot_id}/device/add/',
|
||||||
|
'/inventory/lot/{lot_id}/trade-document/add/',
|
||||||
|
'/inventory/lot/{lot_id}/upload-snapshot/',
|
||||||
|
'/inventory/tag/devices/add/',
|
||||||
|
'/inventory/tag/devices/{id}/del/',
|
||||||
|
'/inventory/upload-snapshot/',
|
||||||
|
'/labels/',
|
||||||
|
'/labels/add/',
|
||||||
|
'/labels/print',
|
||||||
|
'/labels/unnamed/add/',
|
||||||
|
'/labels/{id}/',
|
||||||
'/licences/',
|
'/licences/',
|
||||||
'/lives/',
|
'/lives/',
|
||||||
|
'/login/',
|
||||||
|
'/logout/',
|
||||||
'/lots/',
|
'/lots/',
|
||||||
'/lots/{id}/children',
|
'/lots/{id}/children',
|
||||||
'/lots/{id}/devices',
|
'/lots/{id}/devices',
|
||||||
|
'/manufacturers/',
|
||||||
'/metrics/',
|
'/metrics/',
|
||||||
|
'/profile/',
|
||||||
|
'/set_password/',
|
||||||
'/tags/',
|
'/tags/',
|
||||||
'/tags/{tag_id}/device/{device_id}',
|
'/tags/{tag_id}/device/{device_id}',
|
||||||
'/trade-documents/',
|
'/trade-documents/',
|
||||||
'/users/',
|
'/users/',
|
||||||
'/users/login/',
|
'/users/login/',
|
||||||
'/users/logout/',
|
'/users/logout/',
|
||||||
|
'/versions/',
|
||||||
}
|
}
|
||||||
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
|
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
|
||||||
assert docs['components']['securitySchemes']['bearerAuth'] == {
|
assert docs['components']['securitySchemes']['bearerAuth'] == {
|
||||||
|
@ -68,6 +96,6 @@ def test_api_docs(client: Client):
|
||||||
'description:': 'HTTP Basic scheme',
|
'description:': 'HTTP Basic scheme',
|
||||||
'type': 'http',
|
'type': 'http',
|
||||||
'scheme': 'basic',
|
'scheme': 'basic',
|
||||||
'name': 'Authorization'
|
'name': 'Authorization',
|
||||||
}
|
}
|
||||||
assert len(docs['definitions']) == 132
|
assert len(docs['definitions']) == 132
|
||||||
|
|
|
@ -0,0 +1,844 @@
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
from flask_wtf.csrf import generate_csrf
|
||||||
|
|
||||||
|
from ereuse_devicehub.client import UserClient, UserClientFlask
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.resources.action.models import Snapshot
|
||||||
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
|
from ereuse_devicehub.resources.lot.models import Lot
|
||||||
|
from tests import conftest
|
||||||
|
|
||||||
|
|
||||||
|
def create_device(user, file_name):
|
||||||
|
uri = '/inventory/upload-snapshot/'
|
||||||
|
snapshot = conftest.yaml2json(file_name.split(".json")[0])
|
||||||
|
b_snapshot = bytes(json.dumps(snapshot), 'utf-8')
|
||||||
|
file_snap = (BytesIO(b_snapshot), file_name)
|
||||||
|
user.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'snapshot': file_snap,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
user.post(uri, data=data, content_type="multipart/form-data")
|
||||||
|
|
||||||
|
return Snapshot.query.one()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_login(user: UserClient, app: Devicehub):
|
||||||
|
"""Checks a simple login"""
|
||||||
|
|
||||||
|
client = FlaskClient(app, use_cookies=True)
|
||||||
|
|
||||||
|
body, status, headers = client.get('/login/')
|
||||||
|
body = next(body).decode("utf-8")
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Login to Your Account" in body
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'email': user.email,
|
||||||
|
'password': 'foo',
|
||||||
|
'remember': False,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
body, status, headers = client.post('/login/', data=data, follow_redirects=True)
|
||||||
|
|
||||||
|
body = next(body).decode("utf-8")
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Login to Your Account" not in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_profile(user3: UserClientFlask):
|
||||||
|
body, status = user3.get('/profile/')
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Profile" in body
|
||||||
|
assert user3.email in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_inventory(user3: UserClientFlask):
|
||||||
|
body, status = user3.get('/inventory/device/')
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Unassigned" in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_add_lot(user3: UserClientFlask):
|
||||||
|
body, status = user3.get('/inventory/lot/add/')
|
||||||
|
|
||||||
|
lot_name = "lot1"
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Add a new lot" in body
|
||||||
|
assert lot_name not in body
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'name': lot_name,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
body, status = user3.post('/inventory/lot/add/', data=data)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert lot_name in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_del_lot(user3: UserClientFlask):
|
||||||
|
body, status = user3.get('/inventory/lot/add/')
|
||||||
|
|
||||||
|
lot_name = "lot1"
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Add a new lot" in body
|
||||||
|
assert lot_name not in body
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'name': lot_name,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
body, status = user3.post('/inventory/lot/add/', data=data)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert lot_name in body
|
||||||
|
|
||||||
|
lot = Lot.query.filter_by(name=lot_name).one()
|
||||||
|
uri = '/inventory/lot/{id}/del/'.format(id=lot.id)
|
||||||
|
body, status = user3.get(uri)
|
||||||
|
assert lot_name not in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_update_lot(user3: UserClientFlask):
|
||||||
|
user3.get('/inventory/lot/add/')
|
||||||
|
|
||||||
|
# Add lot
|
||||||
|
data = {
|
||||||
|
'name': "lot1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
user3.post('/inventory/lot/add/', data=data)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'name': "lot2",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
|
||||||
|
lot = Lot.query.one()
|
||||||
|
uri = '/inventory/lot/{uuid}/'.format(uuid=lot.id)
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "lot2" in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_upload_snapshot(user3: UserClientFlask):
|
||||||
|
uri = '/inventory/upload-snapshot/'
|
||||||
|
file_name = 'real-eee-1001pxd.snapshot.12.json'
|
||||||
|
body, status = user3.get(uri)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Select a Snapshot file" in body
|
||||||
|
|
||||||
|
snapshot = conftest.yaml2json(file_name.split(".json")[0])
|
||||||
|
b_snapshot = bytes(json.dumps(snapshot), 'utf-8')
|
||||||
|
file_snap = (BytesIO(b_snapshot), file_name)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'snapshot': file_snap,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
body, status = user3.post(uri, data=data, content_type="multipart/form-data")
|
||||||
|
|
||||||
|
txt = f"{file_name}: Ok"
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert txt in body
|
||||||
|
db_snapthot = Snapshot.query.one()
|
||||||
|
dev = db_snapthot.device
|
||||||
|
assert str(db_snapthot.uuid) == snapshot['uuid']
|
||||||
|
assert dev.type == 'Laptop'
|
||||||
|
assert dev.serial_number == 'b8oaas048285'
|
||||||
|
assert len(dev.actions) == 10
|
||||||
|
assert len(dev.components) == 9
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_inventory_with_device(user3: UserClientFlask):
|
||||||
|
db_snapthot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
body, status = user3.get('/inventory/device/')
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Unassigned" in body
|
||||||
|
assert db_snapthot.device.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_inventory_filter(user3: UserClientFlask):
|
||||||
|
db_snapthot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
|
||||||
|
csrf = generate_csrf()
|
||||||
|
body, status = user3.get(f'/inventory/device/?filter=Laptop&csrf_token={csrf}')
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Unassigned" in body
|
||||||
|
assert db_snapthot.device.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_export_devices(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
uri = "/inventory/export/devices/?ids={id}".format(id=snap.device.devicehub_id)
|
||||||
|
|
||||||
|
body, status = user3.get(uri)
|
||||||
|
assert status == '200 OK'
|
||||||
|
|
||||||
|
export_csv = [line.split(";") for line in body.split("\n")]
|
||||||
|
|
||||||
|
with Path(__file__).parent.joinpath('files').joinpath(
|
||||||
|
'export_devices.csv'
|
||||||
|
).open() as csv_file:
|
||||||
|
obj_csv = csv.reader(csv_file, delimiter=';', quotechar='"')
|
||||||
|
fixture_csv = list(obj_csv)
|
||||||
|
|
||||||
|
assert fixture_csv[0] == export_csv[0], 'Headers are not equal'
|
||||||
|
assert (
|
||||||
|
fixture_csv[1][:19] == export_csv[1][:19]
|
||||||
|
), 'Computer information are not equal'
|
||||||
|
assert fixture_csv[1][20] == export_csv[1][20], 'Computer information are not equal'
|
||||||
|
assert (
|
||||||
|
fixture_csv[1][22:82] == export_csv[1][22:82]
|
||||||
|
), 'Computer information are not equal'
|
||||||
|
assert fixture_csv[1][83] == export_csv[1][83], 'Computer information are not equal'
|
||||||
|
assert (
|
||||||
|
fixture_csv[1][86:] == export_csv[1][86:]
|
||||||
|
), 'Computer information are not equal'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_export_metrics(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
uri = "/inventory/export/metrics/?ids={id}".format(id=snap.device.devicehub_id)
|
||||||
|
|
||||||
|
body, status = user3.get(uri)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert body == ''
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_export_certificates(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
uri = "/inventory/export/certificates/?ids={id}".format(id=snap.device.devicehub_id)
|
||||||
|
|
||||||
|
body, status = user3.get(uri, decode=False)
|
||||||
|
body = str(next(body))
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "PDF-1.5" in body
|
||||||
|
assert 'hts54322' in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_labels(user3: UserClientFlask):
|
||||||
|
body, status = user3.get('/labels/')
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Unique Identifiers Management" in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_add_tag(user3: UserClientFlask):
|
||||||
|
uri = '/labels/add/'
|
||||||
|
body, status = user3.get(uri)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "Add a new Unique Identifier" in body
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'code': "tag1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "tag1" in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_label_details(user3: UserClientFlask):
|
||||||
|
uri = '/labels/add/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'code': "tag1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
|
||||||
|
body, status = user3.get('/labels/tag1/')
|
||||||
|
assert "tag1" in body
|
||||||
|
assert "Print Label" in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_link_tag_to_device(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/labels/add/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'code': "tag1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
|
||||||
|
body, status = user3.get('/inventory/device/')
|
||||||
|
assert "tag1" in body
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'tag': "tag1",
|
||||||
|
'device': dev.id,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/tag/devices/add/'
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
assert len(list(dev.tags)) == 2
|
||||||
|
tags = [tag.id for tag in dev.tags]
|
||||||
|
assert "tag1" in tags
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_unlink_tag_to_device(user3: UserClientFlask):
|
||||||
|
# create device
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
|
||||||
|
# create tag
|
||||||
|
uri = '/labels/add/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'code': "tag1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
|
||||||
|
# link tag to device
|
||||||
|
data = {
|
||||||
|
'tag': "tag1",
|
||||||
|
'device': dev.id,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/tag/devices/add/'
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
|
||||||
|
# unlink tag to device
|
||||||
|
uri = '/inventory/tag/devices/{id}/del/'.format(id=dev.id)
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'code': "tag1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'tag': "tag1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
assert len(list(dev.tags)) == 1
|
||||||
|
tag = list(dev.tags)[0]
|
||||||
|
assert not tag.id == "tag1"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_print_labels(user3: UserClientFlask):
|
||||||
|
# create device
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
|
||||||
|
# create tag
|
||||||
|
uri = '/labels/add/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'code': "tag1",
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
|
||||||
|
# link tag to device
|
||||||
|
data = {
|
||||||
|
'tag': "tag1",
|
||||||
|
'device': dev.id,
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/tag/devices/add/'
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
|
||||||
|
assert len(list(dev.tags)) == 2
|
||||||
|
|
||||||
|
uri = '/labels/print'
|
||||||
|
data = {
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
}
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
path = "/inventory/device/{}/".format(dev.devicehub_id)
|
||||||
|
assert path in body
|
||||||
|
assert "tag1" not in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_add_monitor(user3: UserClientFlask):
|
||||||
|
uri = '/inventory/device/add/'
|
||||||
|
body, status = user3.get(uri)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert "New Device" in body
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Monitor",
|
||||||
|
'serial_number': "AAAAB",
|
||||||
|
'model': "LC27T55",
|
||||||
|
'manufacturer': "Samsung",
|
||||||
|
'generation': 1,
|
||||||
|
'weight': 0.1,
|
||||||
|
'height': 0.1,
|
||||||
|
'depth': 0.1,
|
||||||
|
}
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert 'Device "Monitor" created successfully!' in body
|
||||||
|
dev = Device.query.one()
|
||||||
|
assert dev.type == 'Monitor'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_filter_monitor(user3: UserClientFlask):
|
||||||
|
uri = '/inventory/device/add/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Monitor",
|
||||||
|
'serial_number': "AAAAB",
|
||||||
|
'model': "LC27T55",
|
||||||
|
'manufacturer': "Samsung",
|
||||||
|
'generation': 1,
|
||||||
|
'weight': 0.1,
|
||||||
|
'height': 0.1,
|
||||||
|
'depth': 0.1,
|
||||||
|
}
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
csrf = generate_csrf()
|
||||||
|
|
||||||
|
uri = f'/inventory/device/?filter=Monitor&csrf_token={csrf}'
|
||||||
|
body, status = user3.get(uri)
|
||||||
|
|
||||||
|
assert status == '200 OK'
|
||||||
|
dev = Device.query.one()
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_recycling(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
# fail request
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Allocate",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert dev.actions[-1].type == 'Snapshot'
|
||||||
|
assert 'Action Allocate error!' in body
|
||||||
|
|
||||||
|
# good request
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Recycling",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Recycling'
|
||||||
|
assert 'Action "Recycling" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_error_without_devices(user3: UserClientFlask):
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Recycling",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "",
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert 'Action Recycling error!' in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_use(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Use",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Use'
|
||||||
|
assert 'Action "Use" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_refurbish(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Refurbish",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Refurbish'
|
||||||
|
assert 'Action "Refurbish" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_management(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Management",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Management'
|
||||||
|
assert 'Action "Management" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_allocate(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Allocate",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
'start_time': '2000-01-01',
|
||||||
|
'end_time': '2000-06-01',
|
||||||
|
'end_users': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/allocate/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Allocate'
|
||||||
|
assert 'Action "Allocate" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_allocate_error_required(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Trade",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/allocate/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert dev.actions[-1].type != 'Allocate'
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Allocate",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/allocate/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert 'Action Allocate error' in body
|
||||||
|
assert 'Not a valid date value.' in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_allocate_error_dates(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Allocate",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
'start_time': '2000-06-01',
|
||||||
|
'end_time': '2000-01-01',
|
||||||
|
'end_users': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/allocate/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert 'Action Allocate error' in body
|
||||||
|
assert 'The action cannot finish before it starts.' in body
|
||||||
|
assert dev.actions[-1].type != 'Allocate'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_deallocate(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Allocate",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
'start_time': '2000-01-01',
|
||||||
|
'end_time': '2000-06-01',
|
||||||
|
'end_users': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/allocate/add/'
|
||||||
|
|
||||||
|
user3.post(uri, data=data)
|
||||||
|
assert dev.actions[-1].type == 'Allocate'
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Deallocate",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
'start_time': '2000-01-01',
|
||||||
|
'end_time': '2000-06-01',
|
||||||
|
'end_users': 2,
|
||||||
|
}
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Deallocate'
|
||||||
|
assert 'Action "Deallocate" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_toprepare(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "ToPrepare",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'ToPrepare'
|
||||||
|
assert 'Action "ToPrepare" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_prepare(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Prepare",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Prepare'
|
||||||
|
assert 'Action "Prepare" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_torepair(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "ToRepair",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'ToRepair'
|
||||||
|
assert 'Action "ToRepair" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_ready(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "Ready",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/add/'
|
||||||
|
body, status = user3.post(uri, data=data)
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'Ready'
|
||||||
|
assert 'Action "Ready" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_action_datawipe(user3: UserClientFlask):
|
||||||
|
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
|
||||||
|
dev = snap.device
|
||||||
|
uri = '/inventory/device/'
|
||||||
|
user3.get(uri)
|
||||||
|
|
||||||
|
b_file = b'1234567890'
|
||||||
|
file_name = "my_file.doc"
|
||||||
|
file_upload = (BytesIO(b_file), file_name)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'csrf_token': generate_csrf(),
|
||||||
|
'type': "DataWipe",
|
||||||
|
'severity': "Info",
|
||||||
|
'devices': "{}".format(dev.id),
|
||||||
|
'document-file_name': file_upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
uri = '/inventory/action/datawipe/add/'
|
||||||
|
body, status = user3.post(uri, data=data, content_type="multipart/form-data")
|
||||||
|
assert status == '200 OK'
|
||||||
|
assert dev.actions[-1].type == 'DataWipe'
|
||||||
|
assert 'Action "DataWipe" created successfully!' in body
|
||||||
|
assert dev.devicehub_id in body
|
|
@ -15,7 +15,7 @@ from requests.exceptions import HTTPError
|
||||||
from teal.db import DBError, UniqueViolation
|
from teal.db import DBError, UniqueViolation
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
|
||||||
from ereuse_devicehub.client import UserClient
|
from ereuse_devicehub.client import UserClient, Client
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.devicehub import Devicehub
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
from ereuse_devicehub.parser.models import SnapshotErrors
|
from ereuse_devicehub.parser.models import SnapshotErrors
|
||||||
|
@ -967,15 +967,15 @@ def test_snapshot_wb_lite(user: UserClient):
|
||||||
)
|
)
|
||||||
body, res = user.post(snapshot, uri="/api/inventory/")
|
body, res = user.post(snapshot, uri="/api/inventory/")
|
||||||
|
|
||||||
ssd = [x for x in body['components'] if x['type'] == 'SolidStateDrive'][0]
|
dev = m.Device.query.filter_by(devicehub_id=body['dhid']).one()
|
||||||
|
ssd = [x for x in dev.components if x.type == 'SolidStateDrive'][0]
|
||||||
|
|
||||||
assert body['device']['manufacturer'] == 'lenovo'
|
assert dev.manufacturer == 'lenovo'
|
||||||
# assert body['wbid'] == "LXVC"
|
assert body['sid'] == "MLKO1Y0R55XZM051WQ5KJM01RY44Q"
|
||||||
assert ssd['serialNumber'] == 's35anx0j401001'
|
assert ssd.serial_number == 's35anx0j401001'
|
||||||
assert res.status == '201 CREATED'
|
assert res.status == '201 CREATED'
|
||||||
assert '00:28:f8:a6:d5:7e' in body['device']['hid']
|
assert '00:28:f8:a6:d5:7e' in dev.hid
|
||||||
|
|
||||||
dev = m.Device.query.filter_by(id=body['device']['id']).one()
|
|
||||||
assert dev.actions[0].power_on_hours == 6032
|
assert dev.actions[0].power_on_hours == 6032
|
||||||
errors = SnapshotErrors.query.filter().all()
|
errors = SnapshotErrors.query.filter().all()
|
||||||
assert errors == []
|
assert errors == []
|
||||||
|
@ -987,21 +987,21 @@ def test_snapshot_wb_lite_qemu(user: UserClient):
|
||||||
"""This test check the minimum validation of json that come from snapshot"""
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
|
||||||
snapshot = file_json(
|
snapshot = file_json(
|
||||||
"2022-04-01_06h28m54s_YKPZ27NJ2NMRO4893M4L5NRZV5YJ1_snapshot.json"
|
"qemu-cc9927a9-55ad-4937-b36b-7185147d9fa9.json"
|
||||||
)
|
)
|
||||||
# body, res = user.post(snapshot, res=Snapshot)
|
|
||||||
body, res = user.post(snapshot, uri="/api/inventory/")
|
body, res = user.post(snapshot, uri="/api/inventory/")
|
||||||
|
|
||||||
assert body['wbid'] == "YKPZ27NJ2NMRO4893M4L5NRZV5YJ1"
|
assert body['sid'] == "VL0L5"
|
||||||
assert res.status == '201 CREATED'
|
assert res.status == '201 CREATED'
|
||||||
|
|
||||||
dev = m.Device.query.filter_by(id=body['device']['id']).one()
|
dev = m.Device.query.filter_by(devicehub_id=body['dhid']).one()
|
||||||
assert dev.manufacturer == 'qemu'
|
assert dev.manufacturer == 'qemu'
|
||||||
assert dev.model == 'standard'
|
assert dev.model == 'standard'
|
||||||
assert dev.serial_number is None
|
assert dev.serial_number is None
|
||||||
assert dev.hid is None
|
assert dev.hid is None
|
||||||
assert dev.actions[0].power_on_hours == 0
|
assert dev.actions[0].power_on_hours == 1
|
||||||
assert dev.actions[1].power_on_hours == 0
|
assert dev.components[-1].size == 40960
|
||||||
|
assert dev.components[-1].serial_number == 'qm00001'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.mvp
|
@pytest.mark.mvp
|
||||||
|
@ -1020,7 +1020,7 @@ def test_snapshot_wb_lite_old_snapshots(user: UserClient):
|
||||||
'timestamp': snapshot_11['endTime'],
|
'timestamp': snapshot_11['endTime'],
|
||||||
'type': 'Snapshot',
|
'type': 'Snapshot',
|
||||||
'uuid': str(uuid.uuid4()),
|
'uuid': str(uuid.uuid4()),
|
||||||
'wbid': 'MLKO1',
|
'sid': 'MLKO1',
|
||||||
'software': 'Workbench',
|
'software': 'Workbench',
|
||||||
'version': '2022.03.00',
|
'version': '2022.03.00',
|
||||||
"schema_api": "1.0.0",
|
"schema_api": "1.0.0",
|
||||||
|
@ -1035,36 +1035,172 @@ def test_snapshot_wb_lite_old_snapshots(user: UserClient):
|
||||||
|
|
||||||
body11, res = user.post(snapshot_11, res=Snapshot)
|
body11, res = user.post(snapshot_11, res=Snapshot)
|
||||||
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
||||||
|
dev = m.Device.query.filter_by(devicehub_id=bodyLite['dhid']).one()
|
||||||
components11 = []
|
components11 = []
|
||||||
componentsLite = []
|
componentsLite = []
|
||||||
for c in body11.get('components', []):
|
for c in body11.get('components', []):
|
||||||
if c['type'] in ["HardDrive", "SolidStateDrive"]:
|
if c['type'] in ["HardDrive", "SolidStateDrive"]:
|
||||||
continue
|
continue
|
||||||
components11.append({c.get('model'), c['type'], c.get('manufacturer')})
|
components11.append({c.get('model'), c['type'], c.get('manufacturer')})
|
||||||
for c in bodyLite.get('components', []):
|
for c in dev.components:
|
||||||
componentsLite.append({c.get('model'), c['type'], c.get('manufacturer')})
|
componentsLite.append({c.model, c.type, c.manufacturer})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
assert body11['device'].get('hid') == bodyLite['device'].get('hid')
|
assert body11['device'].get('hid') == dev.hid
|
||||||
if body11['device'].get('hid'):
|
if body11['device'].get('hid'):
|
||||||
assert body11['device']['id'] == bodyLite['device']['id']
|
assert body11['device']['id'] == dev.id
|
||||||
assert body11['device'].get('serialNumber') == bodyLite['device'].get(
|
assert body11['device'].get('serialNumber') == dev.serial_number
|
||||||
'serialNumber'
|
assert body11['device'].get('model') == dev.model
|
||||||
)
|
assert body11['device'].get('manufacturer') == dev.manufacturer
|
||||||
assert body11['device'].get('model') == bodyLite['device'].get('model')
|
|
||||||
assert body11['device'].get('manufacturer') == bodyLite['device'].get(
|
|
||||||
'manufacturer'
|
|
||||||
)
|
|
||||||
|
|
||||||
# wbLite can find more components than wb11
|
# wbLite can find more components than wb11
|
||||||
assert len(components11) <= len(componentsLite)
|
assert len(components11) <= len(componentsLite)
|
||||||
for c in components11:
|
for c in components11:
|
||||||
assert c in componentsLite
|
assert c in componentsLite
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
# import pdb; pdb.set_trace()
|
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_snapshot_lite_error_400(user: UserClient):
|
||||||
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
snapshot_11 = file_json('snapshotErrors.json')
|
||||||
|
lshw = snapshot_11['debug']['lshw']
|
||||||
|
snapshot_lite = {
|
||||||
|
'timestamp': snapshot_11['endTime'],
|
||||||
|
'type': 'Snapshot',
|
||||||
|
'uuid': str(uuid.uuid4()),
|
||||||
|
'sid': 'MLKO1',
|
||||||
|
'software': 'Workbench',
|
||||||
|
'version': '2022.03.00',
|
||||||
|
"schema_api": "1.0.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
user.post(snapshot_lite, uri="/api/inventory/", status=400)
|
||||||
|
|
||||||
|
for k in ['lshw', 'hwinfo', 'smart', 'dmidecode', 'lspci']:
|
||||||
|
data = {
|
||||||
|
'lshw': lshw,
|
||||||
|
'hwinfo': '',
|
||||||
|
'smart': [],
|
||||||
|
'dmidecode': '',
|
||||||
|
'lspci': '',
|
||||||
|
}
|
||||||
|
data.pop(k)
|
||||||
|
snapshot_lite['data'] = data
|
||||||
|
user.post(snapshot_lite, uri="/api/inventory/", status=400)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_snapshot_lite_error_422(user: UserClient):
|
||||||
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
snapshot_11 = file_json('snapshotErrors.json')
|
||||||
|
snapshot_lite = {
|
||||||
|
'timestamp': snapshot_11['endTime'],
|
||||||
|
'type': 'Snapshot',
|
||||||
|
'uuid': str(uuid.uuid4()),
|
||||||
|
'sid': 'MLKO1',
|
||||||
|
'software': 'Workbench',
|
||||||
|
'version': '2022.03.00',
|
||||||
|
"schema_api": "1.0.0",
|
||||||
|
'data': {
|
||||||
|
'lshw': {},
|
||||||
|
'hwinfo': '',
|
||||||
|
'smart': [],
|
||||||
|
'dmidecode': '',
|
||||||
|
'lspci': '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
user.post(snapshot_lite, uri="/api/inventory/", status=422)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_snapshot_lite_minimum(user: UserClient):
|
||||||
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
snapshot_11 = file_json('snapshotErrors.json')
|
||||||
|
lshw = snapshot_11['debug']['lshw']
|
||||||
|
snapshot_lite = {
|
||||||
|
'timestamp': snapshot_11['endTime'],
|
||||||
|
'type': 'Snapshot',
|
||||||
|
'uuid': str(uuid.uuid4()),
|
||||||
|
'sid': 'MLKO1',
|
||||||
|
'software': 'Workbench',
|
||||||
|
'version': '2022.03.00',
|
||||||
|
"schema_api": "1.0.0",
|
||||||
|
'data': {
|
||||||
|
'lshw': lshw,
|
||||||
|
'hwinfo': '',
|
||||||
|
'smart': [],
|
||||||
|
'dmidecode': '',
|
||||||
|
'lspci': '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
||||||
|
assert bodyLite['sid'] == 'MLKO1'
|
||||||
|
assert res.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_snapshot_lite_error_in_components(user: UserClient):
|
||||||
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
snapshot_11 = file_json('snapshotErrorsComponents.json')
|
||||||
|
lshw = snapshot_11['debug']['lshw']
|
||||||
|
snapshot_lite = {
|
||||||
|
'timestamp': snapshot_11['endTime'],
|
||||||
|
'type': 'Snapshot',
|
||||||
|
'uuid': str(uuid.uuid4()),
|
||||||
|
'sid': 'MLKO1',
|
||||||
|
'software': 'Workbench',
|
||||||
|
'version': '2022.03.00',
|
||||||
|
"schema_api": "1.0.0",
|
||||||
|
'data': {
|
||||||
|
'lshw': lshw,
|
||||||
|
'hwinfo': '',
|
||||||
|
'smart': [],
|
||||||
|
'dmidecode': '',
|
||||||
|
'lspci': '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
||||||
|
assert bodyLite['sid'] == 'MLKO1'
|
||||||
|
assert res.status_code == 201
|
||||||
|
|
||||||
|
dev = m.Device.query.filter_by(devicehub_id=bodyLite['dhid']).one()
|
||||||
|
assert 'Motherboard' not in [x.type for x in dev.components]
|
||||||
|
error = SnapshotErrors.query.all()[0]
|
||||||
|
assert 'StopIteration' in error.description
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_snapshot_lite_error_403(client: Client):
|
||||||
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
snapshot_11 = file_json('snapshotErrors.json')
|
||||||
|
lshw = snapshot_11['debug']['lshw']
|
||||||
|
snapshot_lite = {
|
||||||
|
'timestamp': snapshot_11['endTime'],
|
||||||
|
'type': 'Snapshot',
|
||||||
|
'uuid': str(uuid.uuid4()),
|
||||||
|
'sid': 'MLKO1',
|
||||||
|
'software': 'Workbench',
|
||||||
|
'version': '2022.03.00',
|
||||||
|
"schema_api": "1.0.0",
|
||||||
|
'data': {
|
||||||
|
'lshw': lshw,
|
||||||
|
'hwinfo': '',
|
||||||
|
'smart': [],
|
||||||
|
'dmidecode': '',
|
||||||
|
'lspci': '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
client.post(snapshot_lite, uri="/api/inventory/", status=401)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.mvp
|
@pytest.mark.mvp
|
||||||
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
def test_snapshot_errors(user: UserClient):
|
def test_snapshot_errors(user: UserClient):
|
||||||
|
@ -1076,7 +1212,7 @@ def test_snapshot_errors(user: UserClient):
|
||||||
'timestamp': snapshot_11['endTime'],
|
'timestamp': snapshot_11['endTime'],
|
||||||
'type': 'Snapshot',
|
'type': 'Snapshot',
|
||||||
'uuid': str(uuid.uuid4()),
|
'uuid': str(uuid.uuid4()),
|
||||||
'wbid': 'MLKO1',
|
'sid': 'MLKO1',
|
||||||
'software': 'Workbench',
|
'software': 'Workbench',
|
||||||
'version': '2022.03.00',
|
'version': '2022.03.00',
|
||||||
"schema_api": "1.0.0",
|
"schema_api": "1.0.0",
|
||||||
|
@ -1093,25 +1229,22 @@ def test_snapshot_errors(user: UserClient):
|
||||||
body11, res = user.post(snapshot_11, res=Snapshot)
|
body11, res = user.post(snapshot_11, res=Snapshot)
|
||||||
assert SnapshotErrors.query.all() == []
|
assert SnapshotErrors.query.all() == []
|
||||||
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
||||||
|
dev = m.Device.query.filter_by(devicehub_id=bodyLite['dhid']).one()
|
||||||
assert len(SnapshotErrors.query.all()) == 2
|
assert len(SnapshotErrors.query.all()) == 2
|
||||||
|
|
||||||
assert body11['device'].get('hid') == bodyLite['device'].get('hid')
|
assert body11['device'].get('hid') == dev.hid
|
||||||
assert body11['device']['id'] == bodyLite['device']['id']
|
assert body11['device']['id'] == dev.id
|
||||||
assert body11['device'].get('serialNumber') == bodyLite['device'].get(
|
assert body11['device'].get('serialNumber') == dev.serial_number
|
||||||
'serialNumber'
|
assert body11['device'].get('model') == dev.model
|
||||||
)
|
assert body11['device'].get('manufacturer') == dev.manufacturer
|
||||||
assert body11['device'].get('model') == bodyLite['device'].get('model')
|
|
||||||
assert body11['device'].get('manufacturer') == bodyLite['device'].get(
|
|
||||||
'manufacturer'
|
|
||||||
)
|
|
||||||
components11 = []
|
components11 = []
|
||||||
componentsLite = []
|
componentsLite = []
|
||||||
for c in body11['components']:
|
for c in body11['components']:
|
||||||
if c['type'] == "HardDrive":
|
if c['type'] == "HardDrive":
|
||||||
continue
|
continue
|
||||||
components11.append({c['model'], c['type'], c['manufacturer']})
|
components11.append({c['model'], c['type'], c['manufacturer']})
|
||||||
for c in bodyLite['components']:
|
for c in dev.components:
|
||||||
componentsLite.append({c['model'], c['type'], c['manufacturer']})
|
componentsLite.append({c.model, c.type, c.manufacturer})
|
||||||
|
|
||||||
assert len(components11) == len(componentsLite)
|
assert len(components11) == len(componentsLite)
|
||||||
for c in components11:
|
for c in components11:
|
||||||
|
@ -1123,10 +1256,39 @@ def test_snapshot_errors(user: UserClient):
|
||||||
def test_snapshot_errors_timestamp(user: UserClient):
|
def test_snapshot_errors_timestamp(user: UserClient):
|
||||||
"""This test check the minimum validation of json that come from snapshot"""
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
snapshot_lite = file_json('snapshot-error-timestamp.json')
|
snapshot_lite = file_json('snapshot-error-timestamp.json')
|
||||||
|
user.post(snapshot_lite, uri="/api/inventory/", status=422)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.mvp
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_snapshot_errors_no_serial_number(user: UserClient):
|
||||||
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
snapshot_lite = file_json('desktop-amd-bug-no-sn.json')
|
||||||
|
|
||||||
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
||||||
assert res.status_code == 201
|
assert res.status_code == 201
|
||||||
assert len(SnapshotErrors.query.all()) == 1
|
assert len(SnapshotErrors.query.all()) == 0
|
||||||
error = SnapshotErrors.query.all()[0]
|
dev = m.Device.query.filter_by(devicehub_id=bodyLite['dhid']).one()
|
||||||
assert snapshot_lite['wbid'] == error.wbid
|
assert not dev.model
|
||||||
assert user.user['id'] == str(error.owner_id)
|
assert not dev.manufacturer
|
||||||
|
assert not dev.serial_number
|
||||||
|
assert dev.type == "Desktop"
|
||||||
|
for c in dev.components:
|
||||||
|
if not c.type == "HardDrive":
|
||||||
|
continue
|
||||||
|
assert c.serial_number == 'vd051gtf024b4l'
|
||||||
|
assert c.model == "hdt722520dlat80"
|
||||||
|
assert not c.manufacturer
|
||||||
|
test = c.actions[-1]
|
||||||
|
assert test.power_on_hours == 19819
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
def test_snapshot_check_tests_lite(user: UserClient):
|
||||||
|
"""This test check the minimum validation of json that come from snapshot"""
|
||||||
|
snapshot_lite = file_json('test_lite/2022-4-13-19-5_user@dhub.com_b27dbf43-b88a-4505-ae27-10de5a95919e.json')
|
||||||
|
|
||||||
|
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
|
||||||
|
assert res.status_code == 201
|
||||||
|
SnapshotErrors.query.all()
|
||||||
|
dev = m.Device.query.filter_by(devicehub_id=bodyLite['dhid']).one()
|
||||||
|
|
Reference in New Issue