Merge branch 'feature/server-side-render-parser-3021' into feature/list-snapshots-view-#3113

This commit is contained in:
Cayo Puigdefabregas 2022-05-11 17:04:47 +02:00
commit 1baeddbd56
62 changed files with 3415 additions and 1167 deletions

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
ereuse_devicehub/static/vendor
ereuse_devicehub/static/js/print.pdf.js
ereuse_devicehub/static/js/qrcode.js
*.min.js

37
.eslintrc.json Normal file
View File

@ -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
}
}

55
.github/workflows/eslint.yml vendored Normal file
View File

@ -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

8
.gitignore vendored
View File

@ -119,3 +119,11 @@ ENV/
# Temporal dir
tmp/
# NPM modules
node_modules/
yarn.lock
# ESLint Report
eslint_report.json

3
.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 250
}

View File

@ -9,9 +9,30 @@ ml).
## testing
- [added] #219 Add functionality to searchbar (Lots and devices).
- [added] #222 Allow user to update its password.
- [added] #233 Filter in out trades from lots selector.
- [added] #236 Allow select multiple devices in multiple pages.
- [added] #237 Confirmation dialog on apply lots changes.
- [added] #238 Customize labels.
- [added] #242 Add icons in list of devices.
- [added] #244 Select full devices.
- [added] #257 Add functionality to search generic categories like all components.
- [added] #252 new tabs lots and public link in details of one device.
- [changed] #211 Print DHID-QR label for selected devices.
- [changed] #218 Add reactivity to device lots.
- [changed] #220 Add reactive lots list.
- [changed] #232 Set max lots list to 20.
- [changed] #235 Hide trade buttons.
- [changed] #239 Change Tags for Unique Identifier.
- [changed] #247 Change colors.
- [changed] #253 Drop download public links.
- [fixed] #214 Login workflow
- [fixed] #221 Fix responsive issues on frontend.
- [fixed] #223 fix trade lots modal.
- [fixed] #224 fix clickable lots selector not working when click in text.
- [fixed] #254 Fix minor types in frontend.
- [fixed] #255 Fix status column on device list.
## [2.0.0] - 2022-03-15
First server render HTML version. Completely rewrites views of angular JS client on flask.

View File

@ -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)

View File

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

View File

@ -5,7 +5,8 @@ from flask import Blueprint
from flask import current_app as app
from flask import g, jsonify, request
from flask.views import View
from marshmallow import ValidationError
from flask.wrappers import Response
from marshmallow.exceptions import ValidationError
from werkzeug.exceptions import Unauthorized
from ereuse_devicehub.auth import Auth
@ -51,18 +52,22 @@ class InventoryView(LoginMix, SnapshotMix):
self.tmp_snapshots = app.config['TMP_SNAPSHOTS']
self.path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email)
snapshot_json = self.validate(snapshot_json)
try:
self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot()
except ValidationError:
self.response = jsonify('')
self.response.status_code = 201
return self.response
if type(snapshot_json) == Response:
return snapshot_json
self.snapshot_json = ParseSnapshotLsHw(snapshot_json).get_snapshot()
snapshot = self.build()
db.session.add(snapshot)
db.session().final_flush()
db.session.commit()
self.response = self.schema.jsonify(snapshot)
self.response = jsonify(
{
'url': snapshot.device.url.to_text(),
'dhid': snapshot.device.devicehub_id,
'sid': snapshot.sid,
}
)
self.response.status_code = 201
move_json(self.tmp_snapshots, self.path_snapshot, g.user.email)
return self.response
@ -74,11 +79,15 @@ class InventoryView(LoginMix, SnapshotMix):
except ValidationError as err:
txt = "{}".format(err)
uuid = snapshot_json.get('uuid')
sid = snapshot_json.get('sid')
error = SnapshotErrors(
description=txt, snapshot_uuid=uuid, severity=Severity.Error
description=txt, snapshot_uuid=uuid, severity=Severity.Error, sid=sid
)
error.save(commit=True)
raise err
# raise err
self.response = jsonify(err)
self.response.status_code = 400
return self.response
api.add_url_rule('/inventory/', view_func=InventoryView.as_view('inventory'))

View File

@ -2,7 +2,10 @@ from inspect import isclass
from typing import Dict, Iterable, Type, Union
from ereuse_utils.test import JSON, Res
from teal.client import Client as TealClient, Query, Status
from flask.testing import FlaskClient
from flask_wtf.csrf import generate_csrf
from teal.client import Client as TealClient
from teal.client import Query, Status
from werkzeug.exceptions import HTTPException
from ereuse_devicehub.resources import models, schemas
@ -13,110 +16,156 @@ ResourceLike = Union[Type[Union[models.Thing, schemas.Thing]], str]
class Client(TealClient):
"""A client suited for Devicehub main usage."""
def __init__(self, application,
response_wrapper=None,
use_cookies=False,
allow_subdomain_redirects=False):
super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects)
def __init__(
self,
application,
response_wrapper=None,
use_cookies=False,
allow_subdomain_redirects=False,
):
super().__init__(
application, response_wrapper, use_cookies, allow_subdomain_redirects
)
def open(self,
uri: str,
res: ResourceLike = None,
status: Status = 200,
query: Query = tuple(),
accept=JSON,
content_type=JSON,
item=None,
headers: dict = None,
token: str = None,
**kw) -> Res:
def open(
self,
uri: str,
res: ResourceLike = None,
status: Status = 200,
query: Query = tuple(),
accept=JSON,
content_type=JSON,
item=None,
headers: dict = None,
token: str = None,
**kw,
) -> Res:
if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)):
res = res.t
return super().open(uri, res, status, query, accept, content_type, item, headers, token,
**kw)
return super().open(
uri, res, status, query, accept, content_type, item, headers, token, **kw
)
def get(self,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
status: Status = 200,
item: Union[int, str] = None,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
def get(
self,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
status: Status = 200,
item: Union[int, str] = None,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw,
) -> Res:
return super().get(uri, res, query, status, item, accept, headers, token, **kw)
def post(self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
status: Status = 201,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().post(data, uri, res, query, status, content_type, accept, headers, token,
**kw)
def post(
self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
status: Status = 201,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw,
) -> Res:
return super().post(
data, uri, res, query, status, content_type, accept, headers, token, **kw
)
def patch(self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
item: Union[int, str] = None,
status: Status = 200,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().patch(data, uri, res, query, item, status, content_type, accept, token,
headers, **kw)
def patch(
self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
item: Union[int, str] = None,
status: Status = 200,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw,
) -> Res:
return super().patch(
data,
uri,
res,
query,
item,
status,
content_type,
accept,
token,
headers,
**kw,
)
def put(self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
item: Union[int, str] = None,
status: Status = 201,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().put(data, uri, res, query, item, status, content_type, accept, token,
headers, **kw)
def put(
self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
item: Union[int, str] = None,
status: Status = 201,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw,
) -> Res:
return super().put(
data,
uri,
res,
query,
item,
status,
content_type,
accept,
token,
headers,
**kw,
)
def delete(self,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
status: Status = 204,
item: Union[int, str] = None,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().delete(uri, res, query, status, item, accept, headers, token, **kw)
def delete(
self,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
status: Status = 204,
item: Union[int, str] = None,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw,
) -> Res:
return super().delete(
uri, res, query, status, item, accept, headers, token, **kw
)
def login(self, email: str, password: str):
assert isinstance(email, str)
assert isinstance(password, str)
return self.post({'email': email, 'password': password}, '/users/login/', status=200)
return self.post(
{'email': email, 'password': password}, '/users/login/', status=200
)
def get_many(self,
res: ResourceLike,
resources: Iterable[Union[dict, int]],
key: str = None,
**kw) -> Iterable[Union[Dict[str, object], str]]:
def get_many(
self,
res: ResourceLike,
resources: Iterable[Union[dict, int]],
key: str = None,
**kw,
) -> Iterable[Union[Dict[str, object], str]]:
"""Like :meth:`.get` but with many resources."""
return (
self.get(res=res, item=r[key] if key else r, **kw)[0]
for r in resources
self.get(res=res, item=r[key] if key else r, **kw)[0] for r in resources
)
@ -126,33 +175,119 @@ class UserClient(Client):
It will automatically perform login on the first request.
"""
def __init__(self, application,
email: str,
password: str,
response_wrapper=None,
use_cookies=False,
allow_subdomain_redirects=False):
super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects)
def __init__(
self,
application,
email: str,
password: str,
response_wrapper=None,
use_cookies=False,
allow_subdomain_redirects=False,
):
super().__init__(
application, response_wrapper, use_cookies, allow_subdomain_redirects
)
self.email = email # type: str
self.password = password # type: str
self.user = None # type: dict
def open(self,
uri: str,
res: ResourceLike = None,
status: int or HTTPException = 200,
query: Query = tuple(),
accept=JSON,
content_type=JSON,
item=None,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().open(uri, res, status, query, accept, content_type, item, headers,
self.user['token'] if self.user else token, **kw)
def open(
self,
uri: str,
res: ResourceLike = None,
status: int or HTTPException = 200,
query: Query = tuple(),
accept=JSON,
content_type=JSON,
item=None,
headers: dict = None,
token: str = None,
**kw,
) -> Res:
return super().open(
uri,
res,
status,
query,
accept,
content_type,
item,
headers,
self.user['token'] if self.user else token,
**kw,
)
# noinspection PyMethodOverriding
def login(self):
response = super().login(self.email, self.password)
self.user = response[0]
return response
class UserClientFlask:
def __init__(
self,
application,
email: str,
password: str,
response_wrapper=None,
use_cookies=True,
follow_redirects=True,
):
self.email = email
self.password = password
self.follow_redirects = follow_redirects
self.user = None
self.client = FlaskClient(application, use_cookies=use_cookies)
self.client.get('/login/')
data = {
'email': email,
'password': password,
'csrf_token': generate_csrf(),
}
body, status, headers = self.client.post(
'/login/', data=data, follow_redirects=True
)
self.headers = headers
body = next(body).decode("utf-8")
assert "Unassigned" in body
def get(
self,
uri='',
data=None,
follow_redirects=True,
content_type='text/html; charset=utf-8',
decode=True,
**kw,
):
body, status, headers = self.client.get(
uri, data=data, follow_redirects=follow_redirects, headers=self.headers
)
if decode:
body = next(body).decode("utf-8")
return (body, status)
def post(
self,
uri='',
data=None,
follow_redirects=True,
content_type='application/x-www-form-urlencoded',
decode=True,
**kw,
):
body, status, headers = self.client.post(
uri,
data=data,
follow_redirects=follow_redirects,
headers=self.headers,
content_type=content_type,
)
if decode:
body = next(body).decode("utf-8")
return (body, status)

View File

@ -1,7 +1,9 @@
from flask import g
from flask_wtf import FlaskForm
from werkzeug.security import generate_password_hash
from wtforms import BooleanField, EmailField, PasswordField, validators
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import User
@ -59,3 +61,43 @@ class LoginForm(FlaskForm):
self.form_errors.append(self.error_messages['inactive'])
return user.is_active
class PasswordForm(FlaskForm):
password = PasswordField(
'Current Password',
[validators.DataRequired()],
render_kw={'class': "form-control"},
)
newpassword = PasswordField(
'New Password',
[validators.DataRequired()],
render_kw={'class': "form-control"},
)
renewpassword = PasswordField(
'Re-enter New Password',
[validators.DataRequired()],
render_kw={'class': "form-control"},
)
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not is_valid:
return False
if not g.user.check_password(self.password.data):
return False
if self.newpassword.data != self.renewpassword.data:
return False
return True
def save(self, commit=True):
g.user.password = self.newpassword.data
db.session.add(g.user)
if commit:
db.session.commit()
return

View File

@ -58,16 +58,30 @@ from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.user.models import User
DEVICES = {
"All": ["All"],
"All": ["All Devices", "All Components"],
"Computer": [
"All Computers",
"Desktop",
"Laptop",
"Server",
],
"Monitor": ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"],
"Mobile, tablet & smartphone": ["Mobile", "Tablet", "Smartphone", "Cellphone"],
"DataStorage": ["HardDrive", "SolidStateDrive"],
"Monitor": [
"All Monitors",
"ComputerMonitor",
"Monitor",
"TelevisionSet",
"Projector",
],
"Mobile, tablet & smartphone": [
"All Mobile",
"Mobile",
"Tablet",
"Smartphone",
"Cellphone",
],
"DataStorage": ["All DataStorage", "HardDrive", "SolidStateDrive"],
"Accessories & Peripherals": [
"All Peripherals",
"GraphicCard",
"Motherboard",
"NetworkAdapter",
@ -81,26 +95,104 @@ DEVICES = {
],
}
COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer']
COMPONENTS = [
'GraphicCard',
'DataStorage',
'HardDrive',
'DataStorage',
'SolidStateDrive',
'Motherboard',
'NetworkAdapter',
'Processor',
'RamModule',
'SoundCard',
'Display',
'Battery',
'Camera',
]
MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"]
MOBILE = ["Mobile", "Tablet", "Smartphone", "Cellphone"]
DATASTORAGE = ["HardDrive", "SolidStateDrive"]
PERIPHERALS = [
"GraphicCard",
"Motherboard",
"NetworkAdapter",
"Processor",
"RamModule",
"SoundCard",
"Battery",
"Keyboard",
"Mouse",
"MemoryCardReader",
]
class FilterForm(FlaskForm):
filter = SelectField(
'', choices=DEVICES, default="Computer", render_kw={'class': "form-select"}
'', choices=DEVICES, default="All Computers", render_kw={'class': "form-select"}
)
def __init__(self, *args, **kwargs):
def __init__(self, lots, lot_id, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lots = lots
self.lot_id = lot_id
self._get_types()
def _get_types(self):
types_of_devices = [item for sublist in DEVICES.values() for item in sublist]
dev = request.args.get('filter')
self.device = dev if dev in types_of_devices else None
if self.device:
self.filter.data = self.device
self.device_type = dev if dev in types_of_devices else None
if self.device_type:
self.filter.data = self.device_type
def filter_from_lots(self):
if self.lot_id:
self.lot = self.lots.filter(Lot.id == self.lot_id).one()
device_ids = (d.id for d in self.lot.devices)
self.devices = Device.query.filter(Device.id.in_(device_ids))
else:
self.devices = Device.query.filter(Device.owner_id == g.user.id).filter_by(
lots=None
)
def search(self):
self.filter_from_lots()
filter_type = None
if self.device_type:
filter_type = [self.device_type]
else:
# Case without Filter
filter_type = COMPUTERS
if self.device:
return [self.device]
# Generic Filters
if "All Devices" == self.device_type:
filter_type = COMPUTERS + ["Monitor"] + MOBILE
return ['Desktop', 'Laptop', 'Server']
elif "All Components" == self.device_type:
filter_type = COMPONENTS
elif "All Computers" == self.device_type:
filter_type = COMPUTERS
elif "All Monitors" == self.device_type:
filter_type = MONITORS
elif "All Mobile" == self.device_type:
filter_type = MOBILE
elif "All DataStorage" == self.device_type:
filter_type = DATASTORAGE
elif "All Peripherals" == self.device_type:
filter_type = PERIPHERALS
if filter_type:
self.devices = self.devices.filter(Device.type.in_(filter_type))
return self.devices.order_by(Device.updated.desc())
class LotForm(FlaskForm):
@ -208,12 +300,12 @@ class UploadSnapshotForm(FlaskForm, SnapshotMix):
except ValidationError as err:
txt = "{}".format(err)
uuid = snapshot_json.get('uuid')
wbid = snapshot_json.get('wbid')
sid = snapshot_json.get('sid')
error = SnapshotErrors(
description=txt,
snapshot_uuid=uuid,
severity=Severity.Error,
wbid=wbid,
sid=sid,
)
error.save(commit=True)
self.result[filename] = 'Error'
@ -468,7 +560,7 @@ class TagDeviceForm(FlaskForm):
db.session.commit()
class NewActionForm(FlaskForm):
class ActionFormMix(FlaskForm):
name = StringField(
'Name',
[validators.length(max=50)],
@ -500,17 +592,23 @@ class NewActionForm(FlaskForm):
if not is_valid:
return False
self._devices = OrderedSet()
if self.devices.data:
devices = set(self.devices.data.split(","))
self._devices = OrderedSet(
Device.query.filter(Device.id.in_(devices))
.filter(Device.owner_id == g.user.id)
.all()
)
if self.type.data in [None, '']:
return False
if not self._devices:
return False
if not self.devices.data:
return False
self._devices = OrderedSet()
devices = set(self.devices.data.split(","))
self._devices = OrderedSet(
Device.query.filter(Device.id.in_(devices))
.filter(Device.owner_id == g.user.id)
.all()
)
if not self._devices:
return False
return True
@ -543,29 +641,83 @@ class NewActionForm(FlaskForm):
return self.type.data
class AllocateForm(NewActionForm):
start_time = DateField('Start time')
end_time = DateField('End time')
final_user_code = StringField('Final user code', [validators.length(max=50)])
transaction = StringField('Transaction', [validators.length(max=50)])
end_users = IntegerField('End users')
class NewActionForm(ActionFormMix):
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not is_valid:
return False
if self.type.data in ['Allocate', 'Deallocate', 'Trade', 'DataWipe']:
return False
return True
class AllocateForm(ActionFormMix):
start_time = DateField('Start time')
end_time = DateField('End time', [validators.Optional()])
final_user_code = StringField(
'Final user code', [validators.Optional(), validators.length(max=50)]
)
transaction = StringField(
'Transaction', [validators.Optional(), validators.length(max=50)]
)
end_users = IntegerField('End users', [validators.Optional()])
def validate(self, extra_validators=None):
if not super().validate(extra_validators):
return False
if self.type.data not in ['Allocate', 'Deallocate']:
return False
if not self.validate_dates():
return False
if not self.check_devices():
return False
return True
def validate_dates(self):
start_time = self.start_time.data
end_time = self.end_time.data
if not start_time:
self.start_time.errors = ['Not a valid date value.!']
return False
if start_time and end_time and end_time < start_time:
error = ['The action cannot finish before it starts.']
self.start_time.errors = error
self.end_time.errors = error
is_valid = False
return False
if not self.end_users.data:
self.end_users.errors = ["You need to specify a number of users"]
is_valid = False
if not end_time:
self.end_time.data = self.start_time.data
return is_valid
return True
def check_devices(self):
if self.type.data == 'Allocate':
txt = "You need deallocate before allocate this device again"
for device in self._devices:
if device.allocated:
self.devices.errors = [txt]
return False
device.allocated = True
if self.type.data == 'Deallocate':
txt = "Sorry some of this devices are actually deallocate"
for device in self._devices:
if not device.allocated:
self.devices.errors = [txt]
return False
device.allocated = False
return True
class DataWipeDocumentForm(Form):
@ -621,7 +773,7 @@ class DataWipeDocumentForm(Form):
return self._obj
class DataWipeForm(NewActionForm):
class DataWipeForm(ActionFormMix):
document = FormField(DataWipeDocumentForm)
def save(self):
@ -648,7 +800,7 @@ class DataWipeForm(NewActionForm):
return self.instance
class TradeForm(NewActionForm):
class TradeForm(ActionFormMix):
user_from = StringField(
'Supplier',
[validators.Optional()],
@ -695,6 +847,9 @@ class TradeForm(NewActionForm):
email_from = self.user_from.data
email_to = self.user_to.data
if self.type.data != "Trade":
return False
if not self.confirm.data and not self.code.data:
self.code.errors = ["If you don't want to confirm, you need a code"]
is_valid = False

View File

@ -7,10 +7,9 @@ import flask_weasyprint
from flask import Blueprint, g, make_response, request, url_for
from flask.views import View
from flask_login import current_user, login_required
from sqlalchemy import or_
from werkzeug.exceptions import NotFound
from ereuse_devicehub import __version__, messages
from ereuse_devicehub import messages
from ereuse_devicehub.db import db
from ereuse_devicehub.inventory.forms import (
AllocateForm,
@ -32,35 +31,21 @@ from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.views import GenericMixView
devices = Blueprint('inventory', __name__, url_prefix='/inventory')
logger = logging.getLogger(__name__)
class GenericMixView(View):
def get_lots(self):
return (
Lot.query.outerjoin(Trade)
.filter(
or_(
Trade.user_from == g.user,
Trade.user_to == g.user,
Lot.owner_id == g.user.id,
)
)
.distinct()
)
class DeviceListMix(GenericMixView):
decorators = [login_required]
template_name = 'inventory/device_list.html'
def get_context(self, lot_id):
form_filter = FilterForm()
filter_types = form_filter.search()
lots = self.get_lots()
super().get_context()
lots = self.context['lots']
form_filter = FilterForm(lots, lot_id)
devices = form_filter.search()
lot = None
tags = (
Tag.query.filter(Tag.owner_id == current_user.id)
@ -70,10 +55,6 @@ class DeviceListMix(GenericMixView):
if lot_id:
lot = lots.filter(Lot.id == lot_id).one()
devices = lot.devices
if "All" not in filter_types:
devices = [dev for dev in lot.devices if dev.type in filter_types]
devices = sorted(devices, key=lambda x: x.updated, reverse=True)
form_new_action = NewActionForm(lot=lot.id)
form_new_allocate = AllocateForm(lot=lot.id)
form_new_datawipe = DataWipeForm(lot=lot.id)
@ -83,20 +64,6 @@ class DeviceListMix(GenericMixView):
user_from=g.user.email,
)
else:
if "All" in filter_types:
devices = (
Device.query.filter(Device.owner_id == current_user.id)
.filter_by(lots=None)
.order_by(Device.updated.desc())
)
else:
devices = (
Device.query.filter(Device.owner_id == current_user.id)
.filter_by(lots=None)
.filter(Device.type.in_(filter_types))
.order_by(Device.updated.desc())
)
form_new_action = NewActionForm()
form_new_allocate = AllocateForm()
form_new_datawipe = DataWipeForm()
@ -106,21 +73,21 @@ class DeviceListMix(GenericMixView):
if action_devices:
list_devices.extend([int(x) for x in action_devices.split(",")])
self.context = {
'devices': devices,
'lots': lots,
'form_tag_device': TagDeviceForm(),
'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe,
'form_new_trade': form_new_trade,
'form_filter': form_filter,
'form_print_labels': PrintLabelsForm(),
'lot': lot,
'tags': tags,
'list_devices': list_devices,
'version': __version__,
}
self.context.update(
{
'devices': devices,
'form_tag_device': TagDeviceForm(),
'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe,
'form_new_trade': form_new_trade,
'form_filter': form_filter,
'form_print_labels': PrintLabelsForm(),
'lot': lot,
'tags': tags,
'list_devices': list_devices,
}
)
return self.context
@ -136,20 +103,20 @@ class DeviceDetailView(GenericMixView):
template_name = 'inventory/device_detail.html'
def dispatch_request(self, id):
lots = self.get_lots()
self.get_context()
device = (
Device.query.filter(Device.owner_id == current_user.id)
.filter(Device.devicehub_id == id)
.one()
)
context = {
'device': device,
'lots': lots,
'page_title': 'Device {}'.format(device.devicehub_id),
'version': __version__,
}
return flask.render_template(self.template_name, **context)
self.context.update(
{
'device': device,
'page_title': 'Device {}'.format(device.devicehub_id),
}
)
return flask.render_template(self.template_name, **self.context)
class LotCreateView(GenericMixView):
@ -165,17 +132,17 @@ class LotCreateView(GenericMixView):
next_url = url_for('inventory.lotdevicelist', lot_id=form.id)
return flask.redirect(next_url)
lots = self.get_lots()
context = {
'form': form,
'title': self.title,
'lots': lots,
'version': __version__,
}
return flask.render_template(self.template_name, **context)
self.get_context()
self.context.update(
{
'form': form,
'title': self.title,
}
)
return flask.render_template(self.template_name, **self.context)
class LotUpdateView(View):
class LotUpdateView(GenericMixView):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/lot.html'
@ -188,14 +155,14 @@ class LotUpdateView(View):
next_url = url_for('inventory.lotdevicelist', lot_id=id)
return flask.redirect(next_url)
lots = Lot.query.filter(Lot.owner_id == current_user.id)
context = {
'form': form,
'title': self.title,
'lots': lots,
'version': __version__,
}
return flask.render_template(self.template_name, **context)
self.get_context()
self.context.update(
{
'form': form,
'title': self.title,
}
)
return flask.render_template(self.template_name, **self.context)
class LotDeleteView(View):
@ -222,25 +189,26 @@ class UploadSnapshotView(GenericMixView):
template_name = 'inventory/upload_snapshot.html'
def dispatch_request(self, lot_id=None):
lots = self.get_lots()
self.get_context()
form = UploadSnapshotForm()
context = {
'page_title': 'Upload Snapshot',
'lots': lots,
'form': form,
'lot_id': lot_id,
'version': __version__,
}
self.context.update(
{
'page_title': 'Upload Snapshot',
'form': form,
'lot_id': lot_id,
}
)
if form.validate_on_submit():
snapshot, devices = form.save(commit=False)
if lot_id:
lots = self.context['lots']
lot = lots.filter(Lot.id == lot_id).one()
for dev in devices:
lot.devices.add(dev)
db.session.add(lot)
db.session.commit()
return flask.render_template(self.template_name, **context)
return flask.render_template(self.template_name, **self.context)
class DeviceCreateView(GenericMixView):
@ -249,20 +217,21 @@ class DeviceCreateView(GenericMixView):
template_name = 'inventory/device_create.html'
def dispatch_request(self, lot_id=None):
lots = self.get_lots()
self.get_context()
form = NewDeviceForm()
context = {
'page_title': 'New Device',
'lots': lots,
'form': form,
'lot_id': lot_id,
'version': __version__,
}
self.context.update(
{
'page_title': 'New Device',
'form': form,
'lot_id': lot_id,
}
)
if form.validate_on_submit():
snapshot = form.save(commit=False)
next_url = url_for('inventory.devicelist')
if lot_id:
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
lots = self.context['lots']
lot = lots.filter(Lot.id == lot_id).one()
lot.devices.add(snapshot.device)
db.session.add(lot)
@ -271,7 +240,7 @@ class DeviceCreateView(GenericMixView):
messages.success('Device "{}" created successfully!'.format(form.type.data))
return flask.redirect(next_url)
return flask.render_template(self.template_name, **context)
return flask.render_template(self.template_name, **self.context)
class TagLinkDeviceView(View):
@ -287,13 +256,13 @@ class TagLinkDeviceView(View):
return flask.redirect(request.referrer)
class TagUnlinkDeviceView(View):
class TagUnlinkDeviceView(GenericMixView):
methods = ['POST', 'GET']
decorators = [login_required]
template_name = 'inventory/tag_unlink_device.html'
def dispatch_request(self, id):
lots = Lot.query.filter(Lot.owner_id == current_user.id)
self.get_context()
form = TagDeviceForm(delete=True, device=id)
if form.validate_on_submit():
form.remove()
@ -301,14 +270,15 @@ class TagUnlinkDeviceView(View):
next_url = url_for('inventory.devicelist')
return flask.redirect(next_url)
return flask.render_template(
self.template_name,
form=form,
lots=lots,
referrer=request.referrer,
version=__version__,
self.context.update(
{
'form': form,
'referrer': request.referrer,
}
)
return flask.render_template(self.template_name, **self.context)
class NewActionView(View):
methods = ['POST']
@ -317,16 +287,19 @@ class NewActionView(View):
def dispatch_request(self):
self.form = self.form_class()
next_url = self.get_next_url()
if self.form.validate_on_submit():
self.form.save()
messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url()
return flask.redirect(next_url)
messages.error('Action {} error!'.format(self.form.type.data))
return flask.redirect(next_url)
def get_next_url(self):
lot_id = self.form.lot.data
@ -352,10 +325,12 @@ class NewAllocateView(NewActionView, DeviceListMix):
next_url = self.get_next_url()
return flask.redirect(next_url)
lot_id = self.form.lot.data
self.get_context(lot_id)
self.context['form_new_allocate'] = self.form
return flask.render_template(self.template_name, **self.context)
messages.error('Action {} error!'.format(self.form.type.data))
for k, v in self.form.errors.items():
value = ';'.join(v)
messages.error('Action Error {key}: {value}!'.format(key=k, value=value))
next_url = self.get_next_url()
return flask.redirect(next_url)
class NewDataWipeView(NewActionView, DeviceListMix):
@ -374,10 +349,9 @@ class NewDataWipeView(NewActionView, DeviceListMix):
next_url = self.get_next_url()
return flask.redirect(next_url)
lot_id = self.form.lot.data
self.get_context(lot_id)
self.context['form_new_datawipe'] = self.form
return flask.render_template(self.template_name, **self.context)
messages.error('Action {} error!'.format(self.form.type.data))
next_url = self.get_next_url()
return flask.redirect(next_url)
class NewTradeView(NewActionView, DeviceListMix):
@ -396,10 +370,9 @@ class NewTradeView(NewActionView, DeviceListMix):
next_url = self.get_next_url()
return flask.redirect(next_url)
lot_id = self.form.lot.data
self.get_context(lot_id)
self.context['form_new_trade'] = self.form
return flask.render_template(self.template_name, **self.context)
messages.error('Action {} error!'.format(self.form.type.data))
next_url = self.get_next_url()
return flask.redirect(next_url)
class NewTradeDocumentView(View):
@ -411,6 +384,7 @@ class NewTradeDocumentView(View):
def dispatch_request(self, lot_id):
self.form = self.form_class(lot=lot_id)
self.get_context()
if self.form.validate_on_submit():
self.form.save()
@ -418,9 +392,8 @@ class NewTradeDocumentView(View):
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
return flask.redirect(next_url)
return flask.render_template(
self.template_name, form=self.form, title=self.title, version=__version__
)
self.context.update({'form': self.form, 'title': self.title})
return flask.render_template(self.template_name, **self.context)
class ExportsView(View):
@ -432,7 +405,6 @@ class ExportsView(View):
'metrics': self.metrics,
'devices': self.devices_list,
'certificates': self.erasure,
'links': self.public_links,
}
if export_id not in export_ids:
@ -508,19 +480,6 @@ class ExportsView(View):
return self.response_csv(data, "actions_export.csv")
def public_links(self):
# get a csv with the publink links of this devices
data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
cw.writerow(['links'])
host_url = request.host_url
for dev in self.find_devices():
code = dev.devicehub_id
link = [f"{host_url}devices/{code}"]
cw.writerow(link)
return self.response_csv(data, "links.csv")
def erasure(self):
template = self.build_erasure_certificate()
res = flask_weasyprint.render_pdf(

View File

@ -64,10 +64,7 @@ class PrintLabelsForm(FlaskForm):
.all()
)
# print only tags that are DHID
dhids = [x.devicehub_id for x in self._devices]
self._tags = (
Tag.query.filter(Tag.owner_id == g.user.id).filter(Tag.id.in_(dhids)).all()
)
if not self._devices:
return False
return is_valid

View File

@ -27,7 +27,7 @@ class TagListView(View):
context = {
'lots': lots,
'tags': tags,
'page_title': 'Tags Management',
'page_title': 'Unique Identifiers Management',
'version': __version__,
}
return flask.render_template(self.template_name, **context)
@ -102,7 +102,7 @@ class PrintLabelsView(View):
form = PrintLabelsForm()
if form.validate_on_submit():
context['form'] = form
context['tags'] = form._tags
context['devices'] = form._devices
return flask.render_template(self.template_name, **context)
else:
messages.error('Error you need select one or more devices')

View File

@ -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()}')

View File

@ -1,3 +1,4 @@
import logging
import re
from contextlib import suppress
from datetime import datetime
@ -13,7 +14,11 @@ from ereuse_utils.nested_lookup import (
)
from ereuse_devicehub.parser import base2, unit, utils
from ereuse_devicehub.parser.models import SnapshotErrors
from ereuse_devicehub.parser.utils import Dumpeable
from ereuse_devicehub.resources.enums import Severity
logger = logging.getLogger(__name__)
class Device(Dumpeable):
@ -417,7 +422,7 @@ class Computer(Device):
self._ram = None
@classmethod
def run(cls, lshw, hwinfo_raw):
def run(cls, lshw, hwinfo_raw, uuid=None, sid=None):
"""
Gets hardware information from the computer and its components,
like serial numbers or model names, and benchmarks them.
@ -428,17 +433,35 @@ class Computer(Device):
hwinfo = hwinfo_raw.splitlines()
computer = cls(lshw)
components = []
for Component in cls.COMPONENTS:
if Component == Display and computer.type != 'Laptop':
continue # Only get display info when computer is laptop
components.extend(Component.new(lshw=lshw, hwinfo=hwinfo))
components.append(Motherboard.new(lshw, hwinfo))
try:
for Component in cls.COMPONENTS:
if Component == Display and computer.type != 'Laptop':
continue # Only get display info when computer is laptop
components.extend(Component.new(lshw=lshw, hwinfo=hwinfo))
components.append(Motherboard.new(lshw, hwinfo))
computer._ram = sum(
ram.size for ram in components if isinstance(ram, RamModule)
)
except Exception as err:
# if there are any problem with components, save the problem and continue
txt = "Error: Snapshot: {uuid}, sid: {sid}, type_error: {type}, error: {error}".format(
uuid=uuid, sid=sid, type=err.__class__, error=err
)
cls.errors(txt, uuid=uuid, sid=sid)
computer._ram = sum(
ram.size for ram in components if isinstance(ram, RamModule)
)
return computer, components
@classmethod
def errors(cls, txt=None, uuid=None, sid=None, severity=Severity.Error):
if not txt:
return
logger.error(txt)
error = SnapshotErrors(
description=txt, snapshot_uuid=uuid, severity=severity, sid=sid
)
error.save()
def __str__(self) -> str:
specs = super().__str__()
return '{} with {} MB of RAM.'.format(specs, self._ram)

View File

@ -14,7 +14,7 @@ class SnapshotErrors(Thing):
id = Column(BigInteger, Sequence('snapshot_errors_seq'), primary_key=True)
description = Column(CIText(), default='', nullable=False)
wbid = Column(CIText(), nullable=True)
sid = Column(CIText(), nullable=True)
severity = Column(SmallInteger, default=Severity.Info, nullable=False)
snapshot_uuid = Column(UUID(as_uuid=True), nullable=False)
owner_id = db.Column(

View File

@ -3,7 +3,8 @@ import logging
import uuid
from dmidecode import DMIParse
from marshmallow import ValidationError
from flask import request
from marshmallow.exceptions import ValidationError
from ereuse_devicehub.parser import base2
from ereuse_devicehub.parser.computer import Computer
@ -38,7 +39,7 @@ class ParseSnapshot:
"version": "14.0.0",
"endTime": snapshot["timestamp"],
"elapsed": 1,
"wbid": snapshot["wbid"],
"sid": snapshot["sid"],
}
def get_snapshot(self):
@ -120,7 +121,11 @@ class ParseSnapshot:
def get_usb_num(self):
return len(
[u for u in self.dmi.get("Port Connector") if u.get("Port Type") == "USB"]
[
u
for u in self.dmi.get("Port Connector")
if "USB" in u.get("Port Type", "").upper()
]
)
def get_serial_num(self):
@ -128,7 +133,7 @@ class ParseSnapshot:
[
u
for u in self.dmi.get("Port Connector")
if u.get("Port Type") == "SERIAL"
if "SERIAL" in u.get("Port Type", "").upper()
]
)
@ -137,7 +142,7 @@ class ParseSnapshot:
[
u
for u in self.dmi.get("Port Connector")
if u.get("Port Type") == "PCMCIA"
if "PCMCIA" in u.get("Port Type", "").upper()
]
)
@ -314,7 +319,7 @@ class ParseSnapshotLsHw:
def __init__(self, snapshot, default="n/a"):
self.default = default
self.uuid = snapshot.get("uuid")
self.wbid = snapshot.get("wbid")
self.sid = snapshot.get("sid")
self.dmidecode_raw = snapshot["data"]["dmidecode"]
self.smart = snapshot["data"]["smart"]
self.hwinfo_raw = snapshot["data"]["hwinfo"]
@ -339,21 +344,11 @@ class ParseSnapshotLsHw:
"version": "14.0.0",
"endTime": snapshot["timestamp"],
"elapsed": 1,
"wbid": snapshot["wbid"],
"sid": snapshot["sid"],
}
def get_snapshot(self):
try:
return Snapshot().load(self.snapshot_json)
except ValidationError as err:
txt = "{}".format(err)
uuid = self.snapshot_json.get('uuid')
wbid = self.snapshot_json.get('wbid')
error = SnapshotErrors(
description=txt, snapshot_uuid=uuid, severity=Severity.Error, wbid=wbid
)
error.save(commit=True)
raise err
return Snapshot().load(self.snapshot_json)
def parse_hwinfo(self):
hw_blocks = self.hwinfo_raw.split("\n\n")
@ -365,8 +360,24 @@ class ParseSnapshotLsHw:
return x
def set_basic_datas(self):
pc, self.components_obj = Computer.run(self.lshw, self.hwinfo_raw)
self.device = pc.dump()
try:
pc, self.components_obj = Computer.run(
self.lshw, self.hwinfo_raw, self.uuid, self.sid
)
pc = pc.dump()
minimum_hid = None in [pc['manufacturer'], pc['model'], pc['serialNumber']]
if minimum_hid and not self.components_obj:
# if no there are hid and any components return 422
raise Exception
except Exception:
msg = """It has not been possible to create the device because we lack data.
You can find more information at: {}""".format(
request.url_root
)
txt = json.dumps({'sid': self.sid, 'message': msg})
raise ValidationError(txt)
self.device = pc
self.device['uuid'] = self.get_uuid()
def set_components(self):
@ -405,8 +416,10 @@ class ParseSnapshotLsHw:
def get_ram_size(self, ram):
size = ram.get("Size")
if not len(size.split(" ")) == 2:
txt = "Error: Snapshot: {uuid}, tag: {wbid} have this ram Size: {size}".format(
uuid=self.uuid, size=size, wbid=self.wbid
txt = (
"Error: Snapshot: {uuid}, tag: {sid} have this ram Size: {size}".format(
uuid=self.uuid, size=size, sid=self.sid
)
)
self.errors(txt)
return 128
@ -416,8 +429,8 @@ class ParseSnapshotLsHw:
def get_ram_speed(self, ram):
speed = ram.get("Speed", "100")
if not len(speed.split(" ")) == 2:
txt = "Error: Snapshot: {uuid}, tag: {wbid} have this ram Speed: {speed}".format(
uuid=self.uuid, speed=speed, wbid=self.wbid
txt = "Error: Snapshot: {uuid}, tag: {sid} have this ram Speed: {speed}".format(
uuid=self.uuid, speed=speed, sid=self.sid
)
self.errors(txt)
return 100
@ -451,8 +464,8 @@ class ParseSnapshotLsHw:
uuid.UUID(dmi_uuid)
except (ValueError, AttributeError) as err:
self.errors("{}".format(err))
txt = "Error: Snapshot: {uuid} tag: {wbid} have this uuid: {device}".format(
uuid=self.uuid, device=dmi_uuid, wbid=self.wbid
txt = "Error: Snapshot: {uuid} tag: {sid} have this uuid: {device}".format(
uuid=self.uuid, device=dmi_uuid, sid=self.sid
)
self.errors(txt)
dmi_uuid = None
@ -461,6 +474,8 @@ class ParseSnapshotLsHw:
def get_data_storage(self):
for sm in self.smart:
if sm.get('smartctl', {}).get('exit_status') == 1:
continue
model = sm.get('model_name')
manufacturer = None
if model and len(model.split(" ")) > 1:
@ -487,7 +502,7 @@ class ParseSnapshotLsHw:
SSD = 'SolidStateDrive'
HDD = 'HardDrive'
type_dev = x.get('device', {}).get('type')
trim = x.get("trim", {}).get("supported") == "true"
trim = x.get('trim', {}).get("supported") in [True, "true"]
return SSD if type_dev in SSDS or trim else HDD
def get_data_storage_interface(self, x):
@ -496,22 +511,21 @@ class ParseSnapshotLsHw:
DataStorageInterface(interface.upper())
except ValueError as err:
txt = "tag: {}, interface {} is not in DataStorageInterface Enum".format(
interface, self.wbid
interface, self.sid
)
self.errors("{}".format(err))
self.errors(txt)
return "ATA"
def get_data_storage_size(self, x):
type_dev = x.get('device', {}).get('protocol', '').lower()
total_capacity = "{type}_total_capacity".format(type=type_dev)
if not x.get(total_capacity):
total_capacity = x.get('user_capacity', {}).get('bytes')
if not total_capacity:
return 1
# convert bytes to Mb
return x.get(total_capacity) / 1024**2
return total_capacity / 1024**2
def get_test_data_storage(self, smart):
log = "smart_health_information_log"
hours = smart.get("power_on_time", {}).get('hours', 0)
action = {
"status": "Completed without error",
"reallocatedSectorCount": smart.get("reallocated_sector_count", 0),
@ -519,7 +533,8 @@ class ParseSnapshotLsHw:
"assessment": True,
"severity": "Info",
"offlineUncorrectable": smart.get("offline_uncorrectable", 0),
"lifetime": 0,
"lifetime": hours,
"powerOnHours": hours,
"type": "TestDataStorage",
"length": "Short",
"elapsed": 0,
@ -529,11 +544,6 @@ class ParseSnapshotLsHw:
"powerCycleCount": smart.get("power_cycle_count", 0),
}
for k in smart.keys():
if log in k:
action['lifetime'] = smart[k].get("power_on_hours", 0)
action['powerOnHours'] = smart[k].get("power_on_hours", 0)
return action
def errors(self, txt=None, severity=Severity.Info):
@ -543,6 +553,6 @@ class ParseSnapshotLsHw:
logger.error(txt)
self._errors.append(txt)
error = SnapshotErrors(
description=txt, snapshot_uuid=self.uuid, severity=severity, wbid=self.wbid
description=txt, snapshot_uuid=self.uuid, severity=severity, sid=self.sid
)
error.save()

View File

@ -7,11 +7,11 @@ from ereuse_devicehub.resources.schemas import Thing
class Snapshot_lite_data(MarshmallowSchema):
dmidecode = String(required=False)
hwinfo = String(required=False)
smart = List(Dict(), required=False)
lshw = Dict(required=False)
lspci = String(required=False)
dmidecode = String(required=True)
hwinfo = String(required=True)
smart = List(Dict(), required=True)
lshw = Dict(required=True)
lspci = String(required=True)
class Snapshot_lite(Thing):
@ -19,10 +19,10 @@ class Snapshot_lite(Thing):
version = String(required=True)
schema_api = String(required=True)
software = String(required=True)
wbid = String(required=True)
sid = String(required=True)
type = String(required=True)
timestamp = String(required=True)
data = Nested(Snapshot_lite_data)
data = Nested(Snapshot_lite_data, required=True)
@validates_schema
def validate_workbench_version(self, data: dict):

View File

@ -17,12 +17,12 @@ from datetime import datetime, timedelta, timezone
from decimal import ROUND_HALF_EVEN, ROUND_UP, Decimal
from typing import Optional, Set, Union
from uuid import uuid4
from dateutil.tz import tzutc
import inflection
import teal.db
from boltons import urlutils
from citext import CIText
from dateutil.tz import tzutc
from flask import current_app as app
from flask import g
from sortedcontainers import SortedSet
@ -274,7 +274,9 @@ class Action(Thing):
super().__init__(**kwargs)
def __lt__(self, other):
return self.end_time.replace(tzinfo=tzutc()) < other.end_time.replace(tzinfo=tzutc())
return self.end_time.replace(tzinfo=tzutc()) < other.end_time.replace(
tzinfo=tzutc()
)
def __str__(self) -> str:
return '{}'.format(self.severity)
@ -664,7 +666,7 @@ class Snapshot(JoinedWithOneDeviceMixin, ActionWithOneDevice):
elapsed.comment = """For Snapshots made with Workbench, the total amount
of time it took to complete.
"""
wbid = Column(CIText(), nullable=True)
sid = Column(CIText(), nullable=True)
def get_last_lifetimes(self):
"""We get the lifetime and serial_number of the first disk"""

View File

@ -425,7 +425,7 @@ class Snapshot(ActionWithOneDevice):
See docs for more info.
"""
uuid = UUID()
wbid = String(required=False)
sid = String(required=False)
software = EnumField(
SnapshotSoftware,
required=True,

View File

@ -3,7 +3,9 @@ from operator import attrgetter
from uuid import uuid4
from citext import CIText
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
from sqlalchemy import Column
from sqlalchemy import Enum as DBEnum
from sqlalchemy import ForeignKey, Unicode, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import backref, relationship, validates
@ -42,7 +44,7 @@ class Agent(Thing):
__table_args__ = (
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
UniqueConstraint(tax_id, name, name='One tax ID with one name.'),
db.Index('agent_type', type, postgresql_using='hash')
db.Index('agent_type', type, postgresql_using='hash'),
)
@declared_attr
@ -63,7 +65,9 @@ class Agent(Thing):
@property
def actions(self) -> list:
# todo test
return sorted(chain(self.actions_agent, self.actions_to), key=attrgetter('created'))
return sorted(
chain(self.actions_agent, self.actions_to), key=attrgetter('created')
)
@validates('name')
def does_not_contain_slash(self, _, value: str):
@ -76,15 +80,17 @@ class Agent(Thing):
class Organization(JoinedTableMixin, Agent):
default_of = db.relationship(Inventory,
uselist=False,
lazy=True,
backref=backref('org', lazy=True),
# We need to use this as we cannot do Inventory.foreign -> Org
# as foreign keys can only reference to one table
# and we have multiple organization table (one per schema)
foreign_keys=[Inventory.org_id],
primaryjoin=lambda: Organization.id == Inventory.org_id)
default_of = db.relationship(
Inventory,
uselist=False,
lazy=True,
backref=backref('org', lazy=True),
# We need to use this as we cannot do Inventory.foreign -> Org
# as foreign keys can only reference to one table
# and we have multiple organization table (one per schema)
foreign_keys=[Inventory.org_id],
primaryjoin=lambda: Organization.id == Inventory.org_id,
)
def __init__(self, name: str, **kwargs) -> None:
super().__init__(**kwargs, name=name)
@ -97,12 +103,17 @@ class Organization(JoinedTableMixin, Agent):
class Individual(JoinedTableMixin, Agent):
active_org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id))
active_org = relationship(Organization, primaryjoin=active_org_id == Organization.id)
active_org = relationship(
Organization, primaryjoin=active_org_id == Organization.id
)
user_id = Column(UUID(as_uuid=True), ForeignKey(User.id), unique=True)
user = relationship(User,
backref=backref('individuals', lazy=True, collection_class=set),
primaryjoin=user_id == User.id)
user = relationship(
User,
backref=backref('individuals', lazy=True, collection_class=set),
primaryjoin=user_id == User.id,
)
class Membership(Thing):
@ -110,20 +121,29 @@ class Membership(Thing):
For example, because the individual works in or because is a member of.
"""
id = Column(Unicode(), check_lower('id'))
organization_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True)
organization = relationship(Organization,
backref=backref('members', collection_class=set, lazy=True),
primaryjoin=organization_id == Organization.id)
individual_id = Column(UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True)
individual = relationship(Individual,
backref=backref('member_of', collection_class=set, lazy=True),
primaryjoin=individual_id == Individual.id)
def __init__(self, organization: Organization, individual: Individual, id: str = None) -> None:
super().__init__(organization=organization,
individual=individual,
id=id)
id = Column(Unicode(), check_lower('id'))
organization_id = Column(
UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True
)
organization = relationship(
Organization,
backref=backref('members', collection_class=set, lazy=True),
primaryjoin=organization_id == Organization.id,
)
individual_id = Column(
UUID(as_uuid=True), ForeignKey(Individual.id), primary_key=True
)
individual = relationship(
Individual,
backref=backref('member_of', collection_class=set, lazy=True),
primaryjoin=individual_id == Individual.id,
)
def __init__(
self, organization: Organization, individual: Individual, id: str = None
) -> None:
super().__init__(organization=organization, individual=individual, id=id)
__table_args__ = (
UniqueConstraint(id, organization_id, name='One member id per organization.'),
@ -134,6 +154,7 @@ class Person(Individual):
"""A person in the system. There can be several persons pointing to
a real.
"""
pass

View File

@ -1,20 +1,30 @@
import pathlib
import copy
import pathlib
import time
from flask import g
from contextlib import suppress
from fractions import Fraction
from itertools import chain
from operator import attrgetter
from typing import Dict, List, Set
from flask_sqlalchemy import event
from boltons import urlutils
from citext import CIText
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
from flask import g, request
from flask_sqlalchemy import event
from more_itertools import unique_everseen
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
Sequence, SmallInteger, Unicode, inspect, text
from sqlalchemy import BigInteger, Boolean, Column
from sqlalchemy import Enum as DBEnum
from sqlalchemy import (
Float,
ForeignKey,
Integer,
Sequence,
SmallInteger,
Unicode,
inspect,
text,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
@ -22,19 +32,41 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship, validates
from sqlalchemy.util import OrderedSet
from sqlalchemy_utils import ColorType
from stdnum import imei, meid
from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, \
check_lower, check_range, IntEnum
from teal.db import (
CASCADE_DEL,
POLYMORPHIC_ID,
POLYMORPHIC_ON,
URL,
IntEnum,
ResourceNotFound,
check_lower,
check_range,
)
from teal.enums import Layouts
from teal.marshmallow import ValidationError
from teal.resource import url_for_resource
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.utils import hashcode
from ereuse_devicehub.resources.enums import BatteryTechnology, CameraFacing, ComputerChassis, \
DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity, TransferState
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing, listener_reset_field_updated_in_actual_time
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.device.metrics import Metrics
from ereuse_devicehub.resources.enums import (
BatteryTechnology,
CameraFacing,
ComputerChassis,
DataStorageInterface,
DisplayTech,
PrinterTechnology,
RamFormat,
RamInterface,
Severity,
TransferState,
)
from ereuse_devicehub.resources.models import (
STR_SM_SIZE,
Thing,
listener_reset_field_updated_in_actual_time,
)
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.utils import hashcode
def create_code(context):
@ -58,17 +90,21 @@ class Device(Thing):
Devices can contain ``Components``, which are just a type of device
(it is a recursive relationship).
"""
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
id.comment = """The identifier of the device for this database. Used only
internally for software; users should not use this.
"""
type = Column(Unicode(STR_SM_SIZE), nullable=False)
hid = Column(Unicode(), check_lower('hid'), unique=False)
hid.comment = """The Hardware ID (HID) is the ID traceability
hid.comment = (
"""The Hardware ID (HID) is the ID traceability
systems use to ID a device globally. This field is auto-generated
from Devicehub using literal identifiers from the device,
so it can re-generated *offline*.
""" + HID_CONVERSION_DOC
"""
+ HID_CONVERSION_DOC
)
model = Column(Unicode(), check_lower('model'))
model.comment = """The model of the device in lower case.
@ -118,14 +154,18 @@ class Device(Thing):
image = db.Column(db.URL)
image.comment = "An image of the device."
owner_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
owner_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id,
)
owner = db.relationship(User, primaryjoin=owner_id == User.id)
allocated = db.Column(Boolean, default=False)
allocated.comment = "device is allocated or not."
devicehub_id = db.Column(db.CIText(), nullable=True, unique=True, default=create_code)
devicehub_id = db.Column(
db.CIText(), nullable=True, unique=True, default=create_code
)
devicehub_id.comment = "device have a unique code."
active = db.Column(Boolean, default=True)
@ -152,12 +192,12 @@ class Device(Thing):
'image',
'allocated',
'devicehub_id',
'active'
'active',
}
__table_args__ = (
db.Index('device_id', id, postgresql_using='hash'),
db.Index('type_index', type, postgresql_using='hash')
db.Index('type_index', type, postgresql_using='hash'),
)
def __init__(self, **kw) -> None:
@ -187,7 +227,9 @@ class Device(Thing):
for ac in actions_one:
ac.real_created = ac.created
return sorted(chain(actions_multiple, actions_one), key=lambda x: x.real_created)
return sorted(
chain(actions_multiple, actions_one), key=lambda x: x.real_created
)
@property
def problems(self):
@ -196,8 +238,9 @@ class Device(Thing):
There can be up to 3 actions: current Snapshot,
current Physical action, current Trading action.
"""
from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.action.models import Snapshot
from ereuse_devicehub.resources.device import states
actions = set()
with suppress(LookupError, ValueError):
actions.add(self.last_action_of(Snapshot))
@ -217,11 +260,13 @@ class Device(Thing):
"""
# todo ensure to remove materialized values when start using them
# todo or self.__table__.columns if inspect fails
return {c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None)
and c.key not in self._NON_PHYSICAL_PROPS}
return {
c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None)
and c.key not in self._NON_PHYSICAL_PROPS
}
@property
def public_properties(self) -> Dict[str, object or None]:
@ -234,11 +279,13 @@ class Device(Thing):
"""
non_public = ['amount', 'transfer_state', 'receiver_id']
hide_properties = list(self._NON_PHYSICAL_PROPS) + non_public
return {c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None)
and c.key not in hide_properties}
return {
c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs
if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None)
and c.key not in hide_properties
}
@property
def public_actions(self) -> List[object]:
@ -250,6 +297,11 @@ class Device(Thing):
actions.reverse()
return actions
@property
def public_link(self) -> str:
host_url = request.host_url.strip('/')
return "{}{}".format(host_url, self.url.to_text())
@property
def url(self) -> urlutils.URL:
"""The URL where to GET this device."""
@ -260,6 +312,7 @@ class Device(Thing):
"""The last Rate of the device."""
with suppress(LookupError, ValueError):
from ereuse_devicehub.resources.action.models import Rate
return self.last_action_of(Rate)
@property
@ -268,12 +321,14 @@ class Device(Thing):
ever been set."""
with suppress(LookupError, ValueError):
from ereuse_devicehub.resources.action.models import Price
return self.last_action_of(Price)
@property
def last_action_trading(self):
"""which is the last action trading"""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
return self.last_action_of(*states.Trading.actions())
@ -287,6 +342,7 @@ class Device(Thing):
- Management
"""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
return self.last_action_of(*states.Status.actions())
@ -300,6 +356,7 @@ class Device(Thing):
- Management
"""
from ereuse_devicehub.resources.device import states
status_actions = [ac.t for ac in states.Status.actions()]
history = []
for ac in self.actions:
@ -329,13 +386,15 @@ class Device(Thing):
if not hasattr(lot, 'trade'):
return
Status = {0: 'Trade',
1: 'Confirm',
2: 'NeedConfirmation',
3: 'TradeConfirmed',
4: 'Revoke',
5: 'NeedConfirmRevoke',
6: 'RevokeConfirmed'}
Status = {
0: 'Trade',
1: 'Confirm',
2: 'NeedConfirmation',
3: 'TradeConfirmed',
4: 'Revoke',
5: 'NeedConfirmRevoke',
6: 'RevokeConfirmed',
}
trade = lot.trade
user_from = trade.user_from
@ -408,6 +467,7 @@ class Device(Thing):
"""If the actual trading state is an revoke action, this property show
the id of that revoke"""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Trading.actions())
if action.type == 'Revoke':
@ -417,6 +477,7 @@ class Device(Thing):
def physical(self):
"""The actual physical state, None otherwise."""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Physical.actions())
return states.Physical(action.__class__)
@ -425,6 +486,7 @@ class Device(Thing):
def traking(self):
"""The actual traking state, None otherwise."""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Traking.actions())
return states.Traking(action.__class__)
@ -433,6 +495,7 @@ class Device(Thing):
def usage(self):
"""The actual usage state, None otherwise."""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
action = self.last_action_of(*states.Usage.actions())
return states.Usage(action.__class__)
@ -470,8 +533,11 @@ class Device(Thing):
test has been executed.
"""
from ereuse_devicehub.resources.action.models import Test
current_tests = unique_everseen((e for e in reversed(self.actions) if isinstance(e, Test)),
key=attrgetter('type')) # last test of each type
current_tests = unique_everseen(
(e for e in reversed(self.actions) if isinstance(e, Test)),
key=attrgetter('type'),
) # last test of each type
return self._warning_actions(current_tests)
@property
@ -496,7 +562,9 @@ class Device(Thing):
def set_hid(self):
with suppress(TypeError):
self.hid = Naming.hid(self.type, self.manufacturer, self.model, self.serial_number)
self.hid = Naming.hid(
self.type, self.manufacturer, self.model, self.serial_number
)
def last_action_of(self, *types):
"""Gets the last action of the given types.
@ -509,7 +577,9 @@ class Device(Thing):
actions.sort(key=lambda x: x.created)
return next(e for e in reversed(actions) if isinstance(e, types))
except StopIteration:
raise LookupError('{!r} does not contain actions of types {}.'.format(self, types))
raise LookupError(
'{!r} does not contain actions of types {}.'.format(self, types)
)
def which_user_put_this_device_in_trace(self):
"""which is the user than put this device in this trade"""
@ -546,6 +616,32 @@ class Device(Thing):
metrics = Metrics(device=self)
return metrics.get_metrics()
def get_type_logo(self):
# This is used for see one logo of type of device in the frontend
types = {
"Desktop": "bi bi-file-post-fill",
"Laptop": "bi bi-laptop",
"Server": "bi bi-server",
"Processor": "bi bi-cpu",
"RamModule": "bi bi-list",
"Motherboard": "bi bi-cpu-fill",
"NetworkAdapter": "bi bi-hdd-network",
"GraphicCard": "bi bi-brush",
"SoundCard": "bi bi-volume-up-fill",
"Monitor": "bi bi-display",
"Display": "bi bi-display",
"ComputerMonitor": "bi bi-display",
"TelevisionSet": "bi bi-easel",
"TV": "bi bi-easel",
"Projector": "bi bi-camera-video",
"Tablet": "bi bi-tablet-landscape",
"Smartphone": "bi bi-phone",
"Cellphone": "bi bi-telephone",
"HardDrive": "bi bi-hdd-stack",
"SolidStateDrive": "bi bi-hdd",
}
return types.get(self.type, '')
def __lt__(self, other):
return self.id < other.id
@ -571,19 +667,24 @@ class Device(Thing):
class DisplayMixin:
"""Base class for the Display Component and the Monitor Device."""
size = Column(Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True)
size = Column(
Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=True
)
size.comment = """The size of the monitor in inches."""
technology = Column(DBEnum(DisplayTech))
technology.comment = """The technology the monitor uses to display
the image.
"""
resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000),
nullable=True)
resolution_width = Column(
SmallInteger, check_range('resolution_width', 10, 20000), nullable=True
)
resolution_width.comment = """The maximum horizontal resolution the
monitor can natively support in pixels.
"""
resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000),
nullable=True)
resolution_height = Column(
SmallInteger, check_range('resolution_height', 10, 20000), nullable=True
)
resolution_height.comment = """The maximum vertical resolution the
monitor can natively support in pixels.
"""
@ -622,8 +723,12 @@ class DisplayMixin:
def __str__(self) -> str:
if self.size:
return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self)
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(self)
return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(
self
)
return '{0.t} {0.serial_number} 0in ({0.aspect_ratio}) {0.technology}'.format(
self
)
def __format__(self, format_spec: str) -> str:
v = ''
@ -645,6 +750,7 @@ class Computer(Device):
Computer is broadly extended by ``Desktop``, ``Laptop``, and
``Server``. The property ``chassis`` defines it more granularly.
"""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
chassis = Column(DBEnum(ComputerChassis), nullable=True)
chassis.comment = """The physical form of the computer.
@ -652,16 +758,18 @@ class Computer(Device):
It is a subset of the Linux definition of DMI / DMI decode.
"""
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
owner_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
owner_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id,
)
# author = db.relationship(User, primaryjoin=owner_id == User.id)
transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False)
transfer_state = db.Column(
IntEnum(TransferState), default=TransferState.Initial, nullable=False
)
transfer_state.comment = TransferState.__doc__
receiver_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=True)
receiver_id = db.Column(UUID(as_uuid=True), db.ForeignKey(User.id), nullable=True)
receiver = db.relationship(User, primaryjoin=receiver_id == User.id)
uuid = db.Column(UUID(as_uuid=True), nullable=True)
@ -685,22 +793,30 @@ class Computer(Device):
@property
def ram_size(self) -> int:
"""The total of RAM memory the computer has."""
return sum(ram.size or 0 for ram in self.components if isinstance(ram, RamModule))
return sum(
ram.size or 0 for ram in self.components if isinstance(ram, RamModule)
)
@property
def data_storage_size(self) -> int:
"""The total of data storage the computer has."""
return sum(ds.size or 0 for ds in self.components if isinstance(ds, DataStorage))
return sum(
ds.size or 0 for ds in self.components if isinstance(ds, DataStorage)
)
@property
def processor_model(self) -> str:
"""The model of one of the processors of the computer."""
return next((p.model for p in self.components if isinstance(p, Processor)), None)
return next(
(p.model for p in self.components if isinstance(p, Processor)), None
)
@property
def graphic_card_model(self) -> str:
"""The model of one of the graphic cards of the computer."""
return next((p.model for p in self.components if isinstance(p, GraphicCard)), None)
return next(
(p.model for p in self.components if isinstance(p, GraphicCard)), None
)
@property
def network_speeds(self) -> List[int]:
@ -725,16 +841,18 @@ class Computer(Device):
it is not None.
"""
return set(
privacy for privacy in
(hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage))
privacy
for privacy in (
hdd.privacy for hdd in self.components if isinstance(hdd, DataStorage)
)
if privacy
)
@property
def external_document_erasure(self):
"""Returns the external ``DataStorage`` proof of erasure.
"""
"""Returns the external ``DataStorage`` proof of erasure."""
from ereuse_devicehub.resources.action.models import DataWipe
urls = set()
try:
ev = self.last_action_of(DataWipe)
@ -757,8 +875,11 @@ class Computer(Device):
if not self.hid:
return
components = self.components if components_snap is None else components_snap
macs_network = [c.serial_number for c in components
if c.type == 'NetworkAdapter' and c.serial_number is not None]
macs_network = [
c.serial_number
for c in components
if c.type == 'NetworkAdapter' and c.serial_number is not None
]
macs_network.sort()
mac = macs_network[0] if macs_network else ''
if not mac or mac in self.hid:
@ -824,9 +945,13 @@ class Mobile(Device):
"""
ram_size = db.Column(db.Integer, check_range('ram_size', min=128, max=36000))
ram_size.comment = """The total of RAM of the device in MB."""
data_storage_size = db.Column(db.Integer, check_range('data_storage_size', 0, 10 ** 8))
data_storage_size = db.Column(
db.Integer, check_range('data_storage_size', 0, 10**8)
)
data_storage_size.comment = """The total of data storage of the device in MB"""
display_size = db.Column(db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0))
display_size = db.Column(
db.Float(decimal_return_scale=1), check_range('display_size', min=0.1, max=30.0)
)
display_size.comment = """The total size of the device screen"""
@validates('imei')
@ -856,21 +981,24 @@ class Cellphone(Mobile):
class Component(Device):
"""A device that can be inside another device."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
parent_id = Column(BigInteger, ForeignKey(Computer.id))
parent = relationship(Computer,
backref=backref('components',
lazy=True,
cascade=CASCADE_DEL,
order_by=lambda: Component.id,
collection_class=OrderedSet),
primaryjoin=parent_id == Computer.id)
__table_args__ = (
db.Index('parent_index', parent_id, postgresql_using='hash'),
parent = relationship(
Computer,
backref=backref(
'components',
lazy=True,
cascade=CASCADE_DEL,
order_by=lambda: Component.id,
collection_class=OrderedSet,
),
primaryjoin=parent_id == Computer.id,
)
__table_args__ = (db.Index('parent_index', parent_id, postgresql_using='hash'),)
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
"""Gets a component that:
@ -882,11 +1010,16 @@ class Component(Device):
when looking for similar ones.
"""
assert self.hid is None, 'Don\'t use this method with a component that has HID'
component = self.__class__.query \
.filter_by(parent=parent, hid=None, owner_id=self.owner_id,
**self.physical_properties) \
.filter(~Component.id.in_(blacklist)) \
component = (
self.__class__.query.filter_by(
parent=parent,
hid=None,
owner_id=self.owner_id,
**self.physical_properties,
)
.filter(~Component.id.in_(blacklist))
.first()
)
if not component:
raise ResourceNotFound(self.type)
return component
@ -909,7 +1042,8 @@ class GraphicCard(JoinedComponentTableMixin, Component):
class DataStorage(JoinedComponentTableMixin, Component):
"""A device that stores information."""
size = Column(Integer, check_range('size', min=1, max=10 ** 8))
size = Column(Integer, check_range('size', min=1, max=10**8))
size.comment = """The size of the data-storage in MB."""
interface = Column(DBEnum(DataStorageInterface))
@ -920,6 +1054,7 @@ class DataStorage(JoinedComponentTableMixin, Component):
This is, the last erasure performed to the data storage.
"""
from ereuse_devicehub.resources.action.models import EraseBasic
try:
ev = self.last_action_of(EraseBasic)
except LookupError:
@ -934,9 +1069,9 @@ class DataStorage(JoinedComponentTableMixin, Component):
@property
def external_document_erasure(self):
"""Returns the external ``DataStorage`` proof of erasure.
"""
"""Returns the external ``DataStorage`` proof of erasure."""
from ereuse_devicehub.resources.action.models import DataWipe
try:
ev = self.last_action_of(DataWipe)
return ev.document.url.to_text()
@ -986,6 +1121,7 @@ class NetworkAdapter(JoinedComponentTableMixin, NetworkMixin, Component):
class Processor(JoinedComponentTableMixin, Component):
"""The CPU."""
speed = Column(Float, check_range('speed', 0.1, 15))
speed.comment = """The regular CPU speed."""
cores = Column(SmallInteger, check_range('cores', 1, 10))
@ -1000,6 +1136,7 @@ class Processor(JoinedComponentTableMixin, Component):
class RamModule(JoinedComponentTableMixin, Component):
"""A stick of RAM."""
size = Column(SmallInteger, check_range('size', min=128, max=17000))
size.comment = """The capacity of the RAM stick."""
speed = Column(SmallInteger, check_range('speed', min=100, max=10000))
@ -1017,6 +1154,7 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component):
mobiles, smart-watches, and so on; excluding ``ComputerMonitor``
and ``TelevisionSet``.
"""
pass
@ -1032,14 +1170,16 @@ class Battery(JoinedComponentTableMixin, Component):
@property
def capacity(self) -> float:
"""The quantity of """
"""The quantity of"""
from ereuse_devicehub.resources.action.models import MeasureBattery
real_size = self.last_action_of(MeasureBattery).size
return real_size / self.size if real_size and self.size else None
class Camera(Component):
"""The camera of a device."""
focal_length = db.Column(db.SmallInteger)
video_height = db.Column(db.SmallInteger)
video_width = db.Column(db.Integer)
@ -1052,6 +1192,7 @@ class Camera(Component):
class ComputerAccessory(Device):
"""Computer peripherals and similar accessories."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
pass
@ -1074,6 +1215,7 @@ class MemoryCardReader(ComputerAccessory):
class Networking(NetworkMixin, Device):
"""Routers, switches, hubs..."""
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
@ -1119,6 +1261,7 @@ class Microphone(Sound):
class Video(Device):
"""Devices related to video treatment."""
pass
@ -1132,6 +1275,7 @@ class Videoconference(Video):
class Cooking(Device):
"""Cooking devices."""
pass
@ -1183,6 +1327,7 @@ class Manufacturer(db.Model):
Ideally users should use the names from this list when submitting
devices.
"""
name = db.Column(CIText(), primary_key=True)
name.comment = """The normalized name of the manufacturer."""
url = db.Column(URL(), unique=True)
@ -1193,7 +1338,7 @@ class Manufacturer(db.Model):
__table_args__ = (
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
{'schema': 'common'}
{'schema': 'common'},
)
@classmethod
@ -1203,10 +1348,7 @@ class Manufacturer(db.Model):
#: Dialect used to write the CSV
with pathlib.Path(__file__).parent.joinpath('manufacturers.csv').open() as f:
cursor.copy_expert(
'COPY common.manufacturer FROM STDIN (FORMAT csv)',
f
)
cursor.copy_expert('COPY common.manufacturer FROM STDIN (FORMAT csv)', f)
listener_reset_field_updated_in_actual_time(Device)
@ -1218,6 +1360,7 @@ def create_code_tag(mapper, connection, device):
this tag is the same of devicehub_id.
"""
from ereuse_devicehub.resources.tag.model import Tag
if isinstance(device, Computer):
tag = Tag(device_id=device.id, id=device.devicehub_id)
db.session.add(tag)

View File

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

View File

@ -2,37 +2,44 @@ from uuid import uuid4
from flask import current_app as app
from flask_login import UserMixin
from sqlalchemy import Column, Boolean, BigInteger, Sequence
from sqlalchemy import BigInteger, Boolean, Column, Sequence
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy_utils import EmailType, PasswordType
from teal.db import IntEnum
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import SessionType
from ereuse_devicehub.resources.inventory.model import Inventory
from ereuse_devicehub.resources.models import STR_SIZE, Thing
from ereuse_devicehub.resources.enums import SessionType
class User(UserMixin, Thing):
__table_args__ = {'schema': 'common'}
id = Column(UUID(as_uuid=True), default=uuid4, primary_key=True)
email = Column(EmailType, nullable=False, unique=True)
password = Column(PasswordType(max_length=STR_SIZE,
onload=lambda **kwargs: dict(
schemes=app.config['PASSWORD_SCHEMES'],
**kwargs
)))
password = Column(
PasswordType(
max_length=STR_SIZE,
onload=lambda **kwargs: dict(
schemes=app.config['PASSWORD_SCHEMES'], **kwargs
),
)
)
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
active = Column(Boolean, default=True, nullable=False)
phantom = Column(Boolean, default=False, nullable=False)
inventories = db.relationship(Inventory,
backref=db.backref('users', lazy=True, collection_class=set),
secondary=lambda: UserInventory.__table__,
collection_class=set)
inventories = db.relationship(
Inventory,
backref=db.backref('users', lazy=True, collection_class=set),
secondary=lambda: UserInventory.__table__,
collection_class=set,
)
# todo set restriction that user has, at least, one active db
def __init__(self, email, password=None, inventories=None, active=True, phantom=False) -> None:
def __init__(
self, email, password=None, inventories=None, active=True, phantom=False
) -> None:
"""Creates an user.
:param email:
:param password:
@ -44,8 +51,13 @@ class User(UserMixin, Thing):
create during the trade actions
"""
inventories = inventories or {Inventory.current}
super().__init__(email=email, password=password, inventories=inventories,
active=active, phantom=phantom)
super().__init__(
email=email,
password=password,
inventories=inventories,
active=active,
phantom=phantom,
)
def __repr__(self) -> str:
return '<User {0.email}>'.format(self)
@ -73,8 +85,8 @@ class User(UserMixin, Thing):
@property
def get_full_name(self):
# TODO(@slamora) create first_name & last_name fields and use
# them to generate user full name
# TODO(@slamora) create first_name & last_name fields???
# needs to be discussed related to Agent <--> User concepts
return self.email
def check_password(self, password):
@ -84,9 +96,12 @@ class User(UserMixin, Thing):
class UserInventory(db.Model):
"""Relationship between users and their inventories."""
__table_args__ = {'schema': 'common'}
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id), primary_key=True)
inventory_id = db.Column(db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True)
inventory_id = db.Column(
db.Unicode(), db.ForeignKey(Inventory.id), primary_key=True
)
class Session(Thing):
@ -96,9 +111,11 @@ class Session(Thing):
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
type = Column(IntEnum(SessionType), default=SessionType.Internal, nullable=False)
user_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(User.id))
user = db.relationship(User,
backref=db.backref('sessions', lazy=True, collection_class=set),
collection_class=set)
user = db.relationship(
User,
backref=db.backref('sessions', lazy=True, collection_class=set),
collection_class=set,
)
def __str__(self) -> str:
return '{0.token}'.format(self)

View File

@ -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;
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -4,7 +4,7 @@ const Api = {
* @returns get lots
*/
async get_lots() {
var request = await this.doRequest(API_URLS.lots, "GET", null);
const request = await this.doRequest(`${API_URLS.lots}?type=temporary`, "GET", null);
if (request != undefined) return request.items;
throw request;
},
@ -15,7 +15,7 @@ const Api = {
* @returns full detailed device list
*/
async get_devices(ids) {
var request = await this.doRequest(API_URLS.devices + '?filter={"id": [' + ids.toString() + ']}', "GET", null);
const request = await this.doRequest(`${API_URLS.devices }?filter={"id": [${ ids.toString() }]}`, "GET", null);
if (request != undefined) return request.items;
throw request;
},
@ -26,7 +26,7 @@ const Api = {
* @returns full detailed device list
*/
async search_device(id) {
var request = await this.doRequest(API_URLS.devices + '?filter={"devicehub_id": ["' + id + '"]}', "GET", null)
const request = await this.doRequest(`${API_URLS.devices }?filter={"devicehub_id": ["${ id }"]}`, "GET", null)
if (request != undefined) return request.items
throw request
},
@ -37,8 +37,8 @@ const Api = {
* @param {number[]} listDevices list devices id
*/
async devices_add(lotID, listDevices) {
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&");
return await Api.doRequest(queryURL, "POST", null);
const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`;
return Api.doRequest(queryURL, "POST", null);
},
/**
@ -47,8 +47,8 @@ const Api = {
* @param {number[]} listDevices list devices id
*/
async devices_remove(lotID, listDevices) {
var queryURL = API_URLS.devices_modify.replace("UUID", lotID) + "?" + listDevices.map(deviceID => "id=" + deviceID).join("&");
return await Api.doRequest(queryURL, "DELETE", null);
const queryURL = `${API_URLS.devices_modify.replace("UUID", lotID) }?${ listDevices.map(deviceID => `id=${ deviceID}`).join("&")}`;
return Api.doRequest(queryURL, "DELETE", null);
},
/**
@ -59,13 +59,13 @@ const Api = {
* @returns
*/
async doRequest(url, type, body) {
var result;
let result;
try {
result = await $.ajax({
url: url,
type: type,
url,
type,
headers: { "Authorization": API_URLS.Auth_Token },
body: body
body
});
return result;
} catch (error) {

View File

@ -1,15 +1,15 @@
$(document).ready(function() {
$(document).ready(() => {
$("#type").on("change", deviceInputs);
deviceInputs();
})
function deviceInputs() {
if ($("#type").val() == 'Monitor') {
if ($("#type").val() == "Monitor") {
$("#screen").show();
$("#resolution").show();
$("#imei").hide();
$("#meid").hide();
} else if (['Smartphone', 'Cellphone', 'Tablet'].includes($("#type").val())) {
} else if (["Smartphone", "Cellphone", "Tablet"].includes($("#type").val())) {
$("#screen").hide();
$("#resolution").hide();
$("#imei").show();

View File

@ -14,9 +14,9 @@
el = el.trim()
if (all) {
return [...document.querySelectorAll(el)]
} else {
return document.querySelector(el)
}
return document.querySelector(el)
}
/**
@ -34,103 +34,101 @@
* Easy on scroll event listener
*/
const onscroll = (el, listener) => {
el.addEventListener('scroll', listener)
el.addEventListener("scroll", listener)
}
/**
* Sidebar toggle
*/
if (select('.toggle-sidebar-btn')) {
on('click', '.toggle-sidebar-btn', function (e) {
select('body').classList.toggle('toggle-sidebar')
if (select(".toggle-sidebar-btn")) {
on("click", ".toggle-sidebar-btn", (e) => {
select("body").classList.toggle("toggle-sidebar")
})
}
/**
* Search bar toggle
*/
if (select('.search-bar-toggle')) {
on('click', '.search-bar-toggle', function (e) {
select('.search-bar').classList.toggle('search-bar-show')
if (select(".search-bar-toggle")) {
on("click", ".search-bar-toggle", (e) => {
select(".search-bar").classList.toggle("search-bar-show")
})
}
/**
* Navbar links active state on scroll
*/
let navbarlinks = select('#navbar .scrollto', true)
const navbarlinks = select("#navbar .scrollto", true)
const navbarlinksActive = () => {
let position = window.scrollY + 200
const position = window.scrollY + 200
navbarlinks.forEach(navbarlink => {
if (!navbarlink.hash) return
let section = select(navbarlink.hash)
const section = select(navbarlink.hash)
if (!section) return
if (position >= section.offsetTop && position <= (section.offsetTop + section.offsetHeight)) {
navbarlink.classList.add('active')
navbarlink.classList.add("active")
} else {
navbarlink.classList.remove('active')
navbarlink.classList.remove("active")
}
})
}
window.addEventListener('load', navbarlinksActive)
window.addEventListener("load", navbarlinksActive)
onscroll(document, navbarlinksActive)
/**
* Toggle .header-scrolled class to #header when page is scrolled
*/
let selectHeader = select('#header')
const selectHeader = select("#header")
if (selectHeader) {
const headerScrolled = () => {
if (window.scrollY > 100) {
selectHeader.classList.add('header-scrolled')
selectHeader.classList.add("header-scrolled")
} else {
selectHeader.classList.remove('header-scrolled')
selectHeader.classList.remove("header-scrolled")
}
}
window.addEventListener('load', headerScrolled)
window.addEventListener("load", headerScrolled)
onscroll(document, headerScrolled)
}
/**
* Back to top button
*/
let backtotop = select('.back-to-top')
const backtotop = select(".back-to-top")
if (backtotop) {
const toggleBacktotop = () => {
if (window.scrollY > 100) {
backtotop.classList.add('active')
backtotop.classList.add("active")
} else {
backtotop.classList.remove('active')
backtotop.classList.remove("active")
}
}
window.addEventListener('load', toggleBacktotop)
window.addEventListener("load", toggleBacktotop)
onscroll(document, toggleBacktotop)
}
/**
* Initiate tooltips
*/
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
const tooltipTriggerList = [].slice.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
const tooltipList = tooltipTriggerList.map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl))
/**
* Initiate quill editors
*/
if (select('.quill-editor-default')) {
new Quill('.quill-editor-default', {
theme: 'snow'
if (select(".quill-editor-default")) {
new Quill(".quill-editor-default", {
theme: "snow"
});
}
if (select('.quill-editor-bubble')) {
new Quill('.quill-editor-bubble', {
theme: 'bubble'
if (select(".quill-editor-bubble")) {
new Quill(".quill-editor-bubble", {
theme: "bubble"
});
}
if (select('.quill-editor-full')) {
if (select(".quill-editor-full")) {
new Quill(".quill-editor-full", {
modules: {
toolbar: [
@ -181,24 +179,24 @@
/**
* Initiate Bootstrap validation check
*/
var needsValidation = document.querySelectorAll('.needs-validation')
const needsValidation = document.querySelectorAll(".needs-validation")
Array.prototype.slice.call(needsValidation)
.forEach(function (form) {
form.addEventListener('submit', function (event) {
.forEach((form) => {
form.addEventListener("submit", (event) => {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
form.classList.add("was-validated")
}, false)
})
/**
* Initiate Datatables
*/
const datatables = select('.datatable', true)
const datatables = select(".datatable", true)
datatables.forEach(datatable => {
new simpleDatatables.DataTable(datatable);
})
@ -206,45 +204,17 @@
/**
* Autoresize echart charts
*/
const mainContainer = select('#main');
const mainContainer = select("#main");
if (mainContainer) {
setTimeout(() => {
new ResizeObserver(function () {
select('.echart', true).forEach(getEchart => {
new ResizeObserver(() => {
select(".echart", true).forEach(getEchart => {
echarts.getInstanceByDom(getEchart).resize();
})
}).observe(mainContainer);
}, 200);
}
/**
* Select all functionality
*/
var btnSelectAll = document.getElementById("SelectAllBTN");
var tableListCheckboxes = document.querySelectorAll(".deviceSelect");
function itemListCheckChanged(event) {
let isAllChecked = Array.from(tableListCheckboxes).map(itm => itm.checked);
if (isAllChecked.every(bool => bool == true)) {
btnSelectAll.checked = true;
btnSelectAll.indeterminate = false;
} else if (isAllChecked.every(bool => bool == false)) {
btnSelectAll.checked = false;
btnSelectAll.indeterminate = false;
} else {
btnSelectAll.indeterminate = true;
}
}
tableListCheckboxes.forEach(item => {
item.addEventListener("click", itemListCheckChanged);
})
btnSelectAll.addEventListener("click", event => {
let checkedState = event.target.checked;
tableListCheckboxes.forEach(ckeckbox => ckeckbox.checked = checkedState);
})
/**
* Avoid hide dropdown when user clicked inside
*/
@ -256,23 +226,23 @@
* Search form functionality
*/
window.addEventListener("DOMContentLoaded", () => {
var searchForm = document.getElementById("SearchForm")
var inputSearch = document.querySelector("#SearchForm > input")
var doSearch = true
const searchForm = document.getElementById("SearchForm")
const inputSearch = document.querySelector("#SearchForm > input")
const doSearch = true
searchForm.addEventListener("submit", (event) => {
event.preventDefault();
})
let timeoutHandler = setTimeout(() => { }, 1)
let dropdownList = document.getElementById("dropdown-search-list")
let defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
const dropdownList = document.getElementById("dropdown-search-list")
const defaultEmptySearch = document.getElementById("dropdown-search-list").innerHTML
inputSearch.addEventListener("input", (e) => {
clearTimeout(timeoutHandler)
let searchText = e.target.value
if (searchText == '') {
const searchText = e.target.value
if (searchText == "") {
document.getElementById("dropdown-search-list").innerHTML = defaultEmptySearch;
return
}
@ -315,7 +285,7 @@
const device = devices[i];
// See: ereuse_devicehub/resources/device/models.py
var verboseName = `${device.type} ${device.manufacturer} ${device.model}`
const verboseName = `${device.type} ${device.manufacturer} ${device.model}`
const templateString = `
<li>

View File

@ -1,7 +1,7 @@
$(document).ready(function() {
var show_allocate_form = $("#allocateModal").data('show-action-form');
var show_datawipe_form = $("#datawipeModal").data('show-action-form');
var show_trade_form = $("#tradeLotModal").data('show-action-form');
$(document).ready(() => {
const show_allocate_form = $("#allocateModal").data("show-action-form");
const show_datawipe_form = $("#datawipeModal").data("show-action-form");
const show_trade_form = $("#tradeLotModal").data("show-action-form");
if (show_allocate_form != "None") {
$("#allocateModal .btn-primary").show();
newAllocate(show_allocate_form);
@ -17,8 +17,119 @@ $(document).ready(function() {
// $('#selectLot').selectpicker();
})
class TableController {
static #tableRows = () => table.activeRows.length > 0 ? table.activeRows : [];
static #tableRowsPage = () => table.pages[table.rows().dt.currentPage - 1];
/**
* @returns Selected inputs from device list
*/
static getSelectedDevices() {
if (this.#tableRows() == undefined) return [];
return this.#tableRows()
.filter(element => element.querySelector("input").checked)
.map(element => element.querySelector("input"))
}
/**
* @returns Selected inputs in current page from device list
*/
static getAllSelectedDevicesInCurrentPage() {
if (this.#tableRowsPage() == undefined) return [];
return this.#tableRowsPage()
.filter(element => element.querySelector("input").checked)
.map(element => element.querySelector("input"))
}
/**
* @returns All inputs from device list
*/
static getAllDevices() {
if (this.#tableRows() == undefined) return [];
return this.#tableRows()
.map(element => element.querySelector("input"))
}
/**
* @returns All inputs from current page in device list
*/
static getAllDevicesInCurrentPage() {
if (this.#tableRowsPage() == undefined) return [];
return this.#tableRowsPage()
.map(element => element.querySelector("input"))
}
/**
*
* @param {HTMLElement} DOMElements
* @returns Procesed input atributes to an Object class
*/
static ProcessTR(DOMElements) {
return DOMElements.map(element => {
const info = {}
info.checked = element.checked
Object.values(element.attributes).forEach(attrib => { info[attrib.nodeName.replace(/-/g, "_")] = attrib.nodeValue })
return info
})
}
}
/**
* Select all functionality
*/
window.addEventListener("DOMContentLoaded", () => {
const btnSelectAll = document.getElementById("SelectAllBTN");
const alertInfoDevices = document.getElementById("select-devices-info");
function itemListCheckChanged() {
const listDevices = TableController.getAllDevicesInCurrentPage()
const isAllChecked = listDevices.map(itm => itm.checked);
if (isAllChecked.every(bool => bool == true)) {
btnSelectAll.checked = true;
btnSelectAll.indeterminate = false;
alertInfoDevices.innerHTML = `Selected devices: ${TableController.getSelectedDevices().length}
${
TableController.getAllDevices().length != TableController.getSelectedDevices().length
? `<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() {
var devices_count = $(".deviceSelect").filter(':checked').length;
const devices_count = TableController.getSelectedDevices().length;
get_device_list();
if (devices_count == 0) {
$("#addingLotModal .pol").show();
@ -60,7 +171,7 @@ function deviceSelect() {
}
function removeLot() {
var devices = $(".deviceSelect");
const devices = TableController.getAllDevices();
if (devices.length > 0) {
$("#btnRemoveLots .text-danger").show();
} else {
@ -70,10 +181,10 @@ function removeLot() {
}
function removeTag() {
var devices = $(".deviceSelect").filter(':checked');
var devices_id = $.map(devices, function(x) { return $(x).attr('data')});
const devices = TableController.getSelectedDevices();
const devices_id = devices.map(dev => dev.data);
if (devices_id.length == 1) {
var url = "/inventory/tag/devices/"+devices_id[0]+"/del/";
const url = `/inventory/tag/devices/${devices_id[0]}/del/`;
window.location.href = url;
} else {
$("#unlinkTagAlertModal").click();
@ -81,8 +192,8 @@ function removeTag() {
}
function addTag() {
var devices = $(".deviceSelect").filter(':checked');
var devices_id = $.map(devices, function(x) { return $(x).attr('data')});
const devices = TableController.getSelectedDevices();
const devices_id = devices.map(dev => dev.data);
if (devices_id.length == 1) {
$("#addingTagModal .pol").hide();
$("#addingTagModal .btn-primary").show();
@ -95,20 +206,20 @@ function addTag() {
}
function newTrade(action) {
var title = "Trade "
var user_to = $("#user_to").data("email");
var user_from = $("#user_from").data("email");
if (action == 'user_from') {
title = 'Trade Incoming';
$("#user_to").attr('readonly', 'readonly');
$("#user_from").prop('readonly', false);
$("#user_from").val('');
let title = "Trade "
const user_to = $("#user_to").data("email");
const user_from = $("#user_from").data("email");
if (action == "user_from") {
title = "Trade Incoming";
$("#user_to").attr("readonly", "readonly");
$("#user_from").prop("readonly", false);
$("#user_from").val("");
$("#user_to").val(user_to);
} else if (action == 'user_to') {
title = 'Trade Outgoing';
$("#user_from").attr('readonly', 'readonly');
$("#user_to").prop('readonly', false);
$("#user_to").val('');
} else if (action == "user_to") {
title = "Trade Outgoing";
$("#user_from").attr("readonly", "readonly");
$("#user_to").prop("readonly", false);
$("#user_to").val("");
$("#user_from").val(user_from);
}
$("#tradeLotModal #title-action").html(title);
@ -137,44 +248,45 @@ function newDataWipe(action) {
}
function get_device_list() {
var devices = $(".deviceSelect").filter(':checked');
const devices = TableController.getSelectedDevices();
/* Insert the correct count of devices in actions form */
var devices_count = devices.length;
const devices_count = devices.length;
$("#datawipeModal .devices-count").html(devices_count);
$("#allocateModal .devices-count").html(devices_count);
$("#actionModal .devices-count").html(devices_count);
/* Insert the correct value in the input devicesList */
var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(",");
$.map($(".devicesList"), function(x) {
const devices_id = $.map(devices, (x) => $(x).attr("data")).join(",");
$.map($(".devicesList"), (x) => {
$(x).val(devices_id);
});
/* Create a list of devices for human representation */
var computer = {
const computer = {
"Desktop": "<i class='bi bi-building'></i>",
"Laptop": "<i class='bi bi-laptop'></i>",
};
list_devices = devices.map(function (x) {
var typ = $(devices[x]).data("device-type");
var manuf = $(devices[x]).data("device-manufacturer");
var dhid = $(devices[x]).data("device-dhid");
list_devices = devices.map((x) => {
let typ = $(x).data("device-type");
const manuf = $(x).data("device-manufacturer");
const dhid = $(x).data("device-dhid");
if (computer[typ]) {
typ = computer[typ];
};
return typ + " " + manuf + " " + dhid;
return `${typ} ${manuf} ${dhid}`;
});
description = $.map(list_devices, function(x) { return x }).join(", ");
description = $.map(list_devices, (x) => x).join(", ");
$(".enumeration-devices").html(description);
}
function export_file(type_file) {
var devices = $(".deviceSelect").filter(':checked');
var devices_id = $.map(devices, function(x) { return $(x).attr('data-device-dhid')}).join(",");
if (devices_id){
var url = "/inventory/export/"+type_file+"/?ids="+devices_id;
const devices = TableController.getSelectedDevices();
const devices_id = $.map(devices, (x) => $(x).attr("data-device-dhid")).join(",");
if (devices_id) {
const url = `/inventory/export/${type_file}/?ids=${devices_id}`;
window.location.href = url;
} else {
$("#exportAlertModal").click();
@ -194,40 +306,28 @@ async function processSelectedDevices() {
/**
* Manage the actions that will be performed when applying the changes
* @param {*} ev event (Should be a checkbox type)
* @param {string} lotID lot id
* @param {number} deviceID device id
* @param {EventSource} ev event (Should be a checkbox type)
* @param {Lot} lot lot id
* @param {Device[]} selectedDevices device id
*/
manage(event, lotID, deviceListID) {
manage(event, lot, selectedDevices) {
event.preventDefault();
const indeterminate = event.srcElement.indeterminate;
const checked = !event.srcElement.checked;
const lotID = lot.id;
const srcElement = event.srcElement.parentElement.children[0]
const checked = !srcElement.checked;
var found = this.list.filter(list => list.lotID == lotID)[0];
var foundIndex = found != undefined ? this.list.findLastIndex(x => x.lotID == found.lotID) : -1;
const found = this.list.filter(list => list.lot.id == lotID)[0];
if (checked) {
if (found != undefined && found.type == "Remove") {
if (found.isFromIndeterminate == true) {
found.type = "Add";
this.list[foundIndex] = found;
} else {
this.list = this.list.filter(list => list.lotID != lotID);
}
if (found && found.type == "Remove") {
found.type = "Add";
} else {
this.list.push({ type: "Add", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
this.list.push({ type: "Add", lot, devices: selectedDevices });
}
} else if (found && found.type == "Add") {
found.type = "Remove";
} else {
if (found != undefined && found.type == "Add") {
if (found.isFromIndeterminate == true) {
found.type = "Remove";
this.list[foundIndex] = found;
} else {
this.list = this.list.filter(list => list.lotID != lotID);
}
} else {
this.list.push({ type: "Remove", lotID: lotID, devices: deviceListID, isFromIndeterminate: indeterminate });
}
this.list.push({ type: "Remove", lot, devices: selectedDevices });
}
if (this.list.length > 0) {
@ -244,10 +344,10 @@ async function processSelectedDevices() {
* @param {boolean} isError defines if a toast is a error
*/
notifyUser(title, toastText, isError) {
let toast = document.createElement("div");
toast.classList = "alert alert-dismissible fade show " + (isError ? "alert-danger" : "alert-success");
const toast = document.createElement("div");
toast.classList = `alert alert-dismissible fade show ${isError ? "alert-danger" : "alert-success"}`;
toast.attributes["data-autohide"] = !isError;
toast.attributes["role"] = "alert";
toast.attributes.role = "alert";
toast.style = "margin-left: auto; width: fit-content;";
toast.innerHTML = `<strong>${title}</strong><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`;
if (toastText && toastText.length > 0) {
@ -265,21 +365,23 @@ async function processSelectedDevices() {
* Get actions and execute call request to add or remove devices from lots
*/
doActions() {
var requestCount = 0; // This is for count all requested api count, to perform reRender of table device list
let requestCount = 0; // This is for count all requested api count, to perform reRender of table device list
this.list.forEach(async action => {
if (action.type == "Add") {
try {
await Api.devices_add(action.lotID, action.devices);
this.notifyUser("Devices sucefully aded to selected lot/s", "", false);
const devicesIDs = action.devices.filter(dev => !action.lot.devices.includes(dev.id)).map(dev => dev.id)
await Api.devices_add(action.lot.id, devicesIDs);
this.notifyUser("Devices sucefully added to selected lot/s", "", false);
} catch (error) {
this.notifyUser("Failed to add devices to selected lot/s", error.responseJSON.message, true);
}
} else if (action.type == "Remove") {
try {
await Api.devices_remove(action.lotID, action.devices);
const devicesIDs = action.devices.filter(dev => action.lot.devices.includes(dev.id)).map(dev => dev.id)
await Api.devices_remove(action.lot.id, devicesIDs);
this.notifyUser("Devices sucefully removed from selected lot/s", "", false);
} catch (error) {
this.notifyUser("Fail to remove devices from selected lot/s", error.responseJSON.message, true);
this.notifyUser("Failed to remove devices from selected lot/s", error.responseJSON.message, true);
}
}
requestCount += 1
@ -288,6 +390,7 @@ async function processSelectedDevices() {
this.list = [];
}
})
$("#confirmLotsModal").modal("hide"); // Hide dialog when click "Save changes"
document.getElementById("dropDownLotsSelector").classList.remove("show");
}
@ -295,90 +398,163 @@ async function processSelectedDevices() {
* Re-render list in table
*/
async reRenderTable() {
var newRequest = await Api.doRequest(window.location)
const newRequest = await Api.doRequest(window.location)
var tmpDiv = document.createElement("div")
const tmpDiv = document.createElement("div")
tmpDiv.innerHTML = newRequest
var oldTable = Array.from(document.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
var newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
const newTable = Array.from(tmpDiv.querySelectorAll("table.table > tbody > tr .deviceSelect")).map(x => x.attributes["data-device-dhid"].value)
for (let i = 0; i < oldTable.length; i++) {
if (!newTable.includes(oldTable[i])) {
// variable from device_list.html --> See: ereuse_devicehub\templates\inventory\device_list.html (Ln: 411)
table.rows().remove(i)
// https://github.com/fiduswriter/Simple-DataTables/wiki/rows()#removeselect-arraynumber
const rowsToRemove = []
for (let i = 0; i < table.activeRows.length; i++) {
const row = table.activeRows[i];
if (!newTable.includes(row.querySelector("input").attributes["data-device-dhid"].value)) {
rowsToRemove.push(i)
}
}
table.rows().remove(rowsToRemove);
// Restore state of checkbox
const selectAllBTN = document.getElementById("SelectAllBTN");
selectAllBTN.checked = false;
selectAllBTN.indeterminate = false;
}
}
var eventClickActions;
let eventClickActions;
/**
* Generates a list item with a correspondient checkbox state
* @param {String} lotID
* @param {String} lotName
* @param {Array<number>} selectedDevicesIDs
* @param {HTMLElement} target
* @param {Object} lot Lot model server
* @param {Device[]} selectedDevices list selected devices
* @param {HTMLElement} elementTarget
* @param {Action[]} actions
*/
function templateLot(lotID, lot, selectedDevicesIDs, elementTarget, actions) {
function templateLot(lot, selectedDevices, elementTarget, actions) {
elementTarget.innerHTML = ""
const { id, name, state } = lot;
var htmlTemplate = `<input class="form-check-input" type="checkbox" id="${lotID}" style="width: 20px; height: 20px; margin-right: 7px;">
<label class="form-check-label" for="${lotID}">${lot.name}</label>`;
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="${id}">${name}</label>`;
var existLotList = selectedDevicesIDs.map(selected => lot.devices.includes(selected));
var doc = document.createElement('li');
const doc = document.createElement("li");
doc.innerHTML = htmlTemplate;
if (selectedDevicesIDs.length <= 0) {
doc.children[0].disabled = true;
} else if (existLotList.every(value => value == true)) {
doc.children[0].checked = true;
} else if (existLotList.every(value => value == false)) {
doc.children[0].checked = false;
} else {
doc.children[0].indeterminate = true;
switch (state) {
case "true":
doc.children[0].checked = true;
break;
case "false":
doc.children[0].checked = false;
break;
case "indetermined":
doc.children[0].indeterminate = true;
break;
default:
console.warn("This shouldn't be happend: Lot without state: ", lot);
break;
}
doc.children[0].addEventListener('mouseup', (ev) => actions.manage(ev, lotID, selectedDevicesIDs));
doc.children[0].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices));
doc.children[1].addEventListener("mouseup", (ev) => actions.manage(ev, lot, selectedDevices));
elementTarget.append(doc);
}
var listHTML = $("#LotsSelector")
const listHTML = $("#LotsSelector")
// Get selected devices
var selectedDevicesIDs = $.map($(".deviceSelect").filter(':checked'), function (x) { return parseInt($(x).attr('data')) });
if (selectedDevicesIDs.length <= 0) {
listHTML.html('<li style="color: red; text-align: center">No devices selected</li>');
const selectedDevicesID = TableController.ProcessTR(TableController.getSelectedDevices()).map(item => item.data)
if (selectedDevicesID.length <= 0) {
listHTML.html("<li style=\"color: red; text-align: center\">No devices selected</li>");
return;
}
// Initialize Actions list, and set checkbox triggers
var actions = new Actions();
const actions = new Actions();
if (eventClickActions) {
document.getElementById("ApplyDeviceLots").removeEventListener(eventClickActions);
}
eventClickActions = document.getElementById("ApplyDeviceLots").addEventListener("click", () => actions.doActions());
eventClickActions = document.getElementById("ApplyDeviceLots").addEventListener("click", () => {
const modal = $("#confirmLotsModal")
modal.modal({ keyboard: false })
let list_changes_html = "";
// {type: ["Remove" | "Add"], "LotID": string, "devices": number[]}
actions.list.forEach(action => {
let type;
let devices;
if (action.type == "Add") {
type = "success";
devices = action.devices.filter(dev => !action.lot.devices.includes(dev.id)) // Only show affected devices
} else {
type = "danger";
devices = action.devices.filter(dev => action.lot.devices.includes(dev.id)) // Only show affected devices
}
list_changes_html += `
<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");
try {
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);
var lots = await Api.get_lots();
listHTML.html("<li style=\"text-align: center\"><div class=\"spinner-border text-info\" style=\"margin: auto\" role=\"status\"></div></li>")
const selectedDevices = await Api.get_devices(selectedDevicesID);
let lots = await Api.get_lots();
lots = lots.map(lot => {
lot.devices = devices
lot.devices = selectedDevices
.filter(device => device.lots.filter(devicelot => devicelot.id == lot.id).length > 0)
.map(device => parseInt(device.id));
switch (lot.devices.length) {
case 0:
lot.state = "false";
break;
case selectedDevicesID.length:
lot.state = "true";
break;
default:
lot.state = "indetermined";
break;
}
return lot;
})
listHTML.html('');
lots.forEach(lot => templateLot(lot.id, lot, selectedDevicesIDs, listHTML, actions));
let lotsList = [];
lotsList.push(lots.filter(lot => lot.state == "true").sort((a, b) => a.name.localeCompare(b.name)));
lotsList.push(lots.filter(lot => lot.state == "indetermined").sort((a, b) => a.name.localeCompare(b.name)));
lotsList.push(lots.filter(lot => lot.state == "false").sort((a, b) => a.name.localeCompare(b.name)));
lotsList = lotsList.flat(); // flat array
listHTML.html("");
lotsList.forEach(lot => templateLot(lot, selectedDevices, listHTML, actions));
} catch (error) {
console.log(error);
listHTML.html('<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>");
}
}

View File

@ -1,8 +1,10 @@
$(document).ready(function() {
STORAGE_KEY = 'tag-spec-key';
$("#printerType").on("change", change_size);
$(".form-check-input").on("change", change_check);
change_size();
load_size();
load_settings();
change_check();
})
function qr_draw(url, id) {
@ -16,27 +18,43 @@ function qr_draw(url, id) {
});
}
function save_size() {
function save_settings() {
var height = $("#height-tag").val();
var width = $("#width-tag").val();
var sizePreset = $("#printerType").val();
var data = {"height": height, "width": width, "sizePreset": sizePreset};
data['dhid'] = $("#dhidCheck").prop('checked');
data['qr'] = $("#qrCheck").prop('checked');
data['serial_number'] = $("#serialNumberCheck").prop('checked');
data['manufacturer'] = $("#manufacturerCheck").prop('checked');
data['model'] = $("#modelCheck").prop('checked');
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function load_size() {
function load_settings() {
var data = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (data){
$("#height-tag").val(data.height);
$("#width-tag").val(data.width);
$("#printerType").val(data.sizePreset);
$("#qrCheck").prop('checked', data.qr);
$("#dhidCheck").prop('checked', data.dhid);
$("#serialNumberCheck").prop('checked', data.serial_number);
$("#manufacturerCheck").prop('checked', data.manufacturer);
$("#modelCheck").prop('checked', data.model);
};
}
function reset_size() {
function reset_settings() {
localStorage.removeItem(STORAGE_KEY);
$("#printerType").val('brotherSmall');
$("#qrCheck").prop('checked', true);
$("#dhidCheck").prop('checked', true);
$("#serialNumberCheck").prop('checked', false);
$("#manufacturerCheck").prop('checked', false);
$("#modelCheck").prop('checked', false);
change_size();
change_check();
}
function change_size() {
@ -50,29 +68,101 @@ function change_size() {
}
}
function change_check() {
if ($("#dhidCheck").prop('checked')) {
$(".dhid").show();
} else {
$(".dhid").hide();
}
if ($("#serialNumberCheck").prop('checked')) {
$(".serial_number").show();
} else {
$(".serial_number").hide();
}
if ($("#manufacturerCheck").prop('checked')) {
$(".manufacturer").show();
} else {
$(".manufacturer").hide();
}
if ($("#modelCheck").prop('checked')) {
$(".model").show();
} else {
$(".model").hide();
}
if ($("#qrCheck").prop('checked')) {
$(".qr").show();
} else {
$(".qr").hide();
}
}
function printpdf() {
var border = 2;
var line = 5;
var height = parseInt($("#height-tag").val());
var width = parseInt($("#width-tag").val());
img_side = Math.min(height, width) - 2*border;
var img_side = Math.min(height, width) - 2*border;
max_tag_side = (Math.max(height, width)/2) + border;
if (max_tag_side < img_side) {
max_tag_side = img_side+ 2*border;
max_tag_side = img_side + 2*border;
};
min_tag_side = (Math.min(height, width)/2) + border;
var last_tag_code = '';
if ($("#serialNumberCheck").prop('checked')) {
height += line;
};
if ($("#manufacturerCheck").prop('checked')) {
height += line;
};
if ($("#modelCheck").prop('checked')) {
height += line;
};
var pdf = new jsPDF('l', 'mm', [width, height]);
$(".tag").map(function(x, y) {
if (x != 0){
pdf.addPage();
console.log(x)
};
var space = line + border;
if ($("#qrCheck").prop('checked')) {
space += img_side;
}
var tag = $(y).text();
last_tag_code = tag;
var imgData = $('#'+tag+' img').attr("src");
pdf.addImage(imgData, 'PNG', border, border, img_side, img_side);
pdf.text(tag, max_tag_side, min_tag_side);
if ($("#qrCheck").prop('checked')) {
var imgData = $('#'+tag+' img').attr("src");
pdf.addImage(imgData, 'PNG', border, border, img_side, img_side);
};
if ($("#dhidCheck").prop('checked')) {
if ($("#qrCheck").prop('checked')) {
pdf.setFontSize(15);
pdf.text(tag, max_tag_side, min_tag_side);
} else {
pdf.setFontSize(15);
pdf.text(tag, border, space);
space += line;
}
};
if ($("#serialNumberCheck").prop('checked')) {
var sn = $(y).data('serial-number');
pdf.setFontSize(12);
pdf.text(sn, border, space);
space += line;
};
if ($("#manufacturerCheck").prop('checked')) {
var sn = $(y).data('manufacturer');
pdf.setFontSize(12);
pdf.text(sn, border, space);
space += line;
};
if ($("#modelCheck").prop('checked')) {
var sn = $(y).data('model');
pdf.setFontSize(8);
pdf.text(sn, border, space);
space += line;
};
});
pdf.save('Tag_'+last_tag_code+'.pdf');

View File

@ -29,6 +29,7 @@
<!-- Template Main CSS File -->
<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

View File

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

View File

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

View File

@ -27,248 +27,40 @@
</div>
<div class="col-xl-8 d-none"><!-- TODO (hidden until is implemented )-->
<div class="col-xl-8">
<div class="card">
<div class="card-body pt-3">
<!-- Bordered Tabs -->
<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">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#profile-change-password">Change Password</button>
</li>
</ul>
<div class="tab-content pt-2">
<div class="tab-pane fade show active profile-overview" id="profile-overview">
<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">
<div class="tab-pane fade show active pt-3" id="profile-change-password">
<!-- 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">
<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">
<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 class="row mb-3">
<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>
{% endif %}
{% endfor %}
<div class="text-center">
<button type="submit" class="btn btn-primary">Change Password</button>
</div>

View File

@ -3,14 +3,14 @@
<div class="modal-content">
<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>
</div>
<form action="{{ url_for('inventory.tag_devices_add') }}" method="post">
{{ form_tag_device.csrf_token }}
<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">
{% for tag in tags %}
<option value="{{ tag.id }}">{{ tag.id }}</option>
@ -18,7 +18,7 @@
</select>
<input class="devicesList" type="hidden" name="device" />
<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>
</div>

View File

@ -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>

View File

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

View File

@ -7,7 +7,7 @@
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
{% if not lot %}
<li class="breadcrumb-item active">Unassgined</li>
<li class="breadcrumb-item active">Unassigned</li>
{% elif lot.is_temporary %}
<li class="breadcrumb-item active">Temporary Lot</li>
<li class="breadcrumb-item active">{{ lot.name }}</li>
@ -32,21 +32,25 @@
<div class="card-body pt-3">
<!-- Bordered Tabs -->
<div class="d-flex align-items-center justify-content-between">
<h3><a href="{{ url_for('inventory.lot_edit', id=lot.id) }}">{{ lot.name }}</a></h3>
<div class="d-flex align-items-center justify-content-between row">
<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 %}
<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')">
<i class="bi bi-arrow-down-right"></i> Add supplier
</a>
<a class="me-2" href="javascript:newTrade('user_to')">
<i class="bi bi-arrow-up-right"></i> Add receiver
</a>
{% endif %}{# <!-- /end TODO --> #}
<a class="text-danger" href="javascript:removeLot()">
<i class="bi bi-trash"></i> Delete Lot
</a>
<span class="d-none" id="activeRemoveLotModal" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"></span>
{% endif %}
</div>
</div>
@ -69,7 +73,7 @@
</ul>
{% 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">
<label class="btn btn-primary " for="SelectAllBTN"><input type="checkbox" id="SelectAllBTN" autocomplete="off"></label>
<div class="btn-group dropdown ml-1">
@ -79,9 +83,9 @@
<span class="caret"></span>
</button>
<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>
<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>
<a href="#" class="dropdown-item" id="ApplyDeviceLots">
@ -91,7 +95,7 @@
</li>
</ul>
</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">
<i class="bi bi-plus"></i>
New Actions
@ -178,7 +182,7 @@
</ul>
</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">
<i class="bi bi-reply"></i>
Exports
@ -197,12 +201,6 @@
Metrics Spreadsheet
</a>
</li>
<li>
<a href="javascript:export_file('links')" class="dropdown-item">
<i class="bi bi-link-45deg"></i>
Public Links
</a>
</li>
<li>
<a href="javascript:export_file('certificates')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
@ -212,26 +210,43 @@
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnUniqueID" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i>
Tags
Unique Identifiers
</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">
<ul class="dropdown-menu" aria-labelledby="btnUniqueID">
<li>
<a href="javascript:addTag()" class="dropdown-item">
<i class="bi bi-plus"></i>
Add Tag to selected Device
Add Unique Identifier to selected Device
</a>
</li>
<li>
<a href="javascript:removeTag()" class="dropdown-item">
<i class="bi bi-x"></i>
Remove Tag from selected Device
Remove Unique Identifier from selected Device
</a>
</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>
<form id="print_labels" method="post" action="{{ url_for('labels.print_labels') }}">
{% for f in form_print_labels %}
@ -246,7 +261,7 @@
</ul>
</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">
<i class="bi bi-laptop"></i>
New Device
@ -293,6 +308,10 @@
</div>
{% endif %}
<div id="select-devices-info" class="alert alert-info mb-0 mt-3 d-none" role="alert">
If this text is showing is because there are an error
</div>
<div class="tab-content pt-2">
<form method="get">
<div class="d-flex mt-4 mb-4">
@ -314,7 +333,7 @@
<th scope="col">Select</th>
<th scope="col">Title</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" data-type="date" data-format="DD-MM-YYYY">Update</th>
</tr>
@ -325,13 +344,16 @@
<td>
<input type="checkbox" class="deviceSelect" data="{{ dev.id }}"
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 %}
checked="checked"
{% endif %}
/>
</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)}}">
{{ dev.verbose_name }}
</a>
@ -347,7 +369,7 @@
{% if not loop.last %},{% endif %}
{% endfor %}
</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>
</tr>
{% endfor %}
@ -404,10 +426,13 @@
{% include "inventory/trade.html" %}
{% include "inventory/alert_export_error.html" %}
{% include "inventory/alert_unlink_tag_error.html" %}
{% include "inventory/alert_lots_changes.html" %}
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table")
const table = new simpleDatatables.DataTable("table", {
perPage: 20
})
</script>
<script src="{{ url_for('static', filename='js/main_inventory.js') }}"></script>
{% endblock main %}

View File

@ -18,8 +18,8 @@
<div class="card-body">
<div class="pt-4 pb-2">
<h1 class="card-title text-center pb-0 fs-4">Unlink Tag from Device</h1>
<p class="text-center small">Please enter a code for the tag.</p>
<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 unique identifier.</p>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
@ -33,10 +33,10 @@
{{ form.csrf_token }}
<div>
<label for="tag" class="form-label">Tag</label>
<label for="tag" class="form-label">Unique Identifier</label>
<div class="input-group has-validation">
{{ form.tag(class_="form-control") }}
<div class="invalid-feedback">Please select tag.</div>
<div class="invalid-feedback">Please select unique identifier.</div>
</div>
{% if form.tag.errors %}
<p class="text-danger">

View File

@ -5,8 +5,8 @@
<h1>Inventory</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Tag management</a></li>
<li class="breadcrumb-item active">Tag details {{ tag.id }}</li>
<li class="breadcrumb-item"><a href="{{ url_for('labels.label_list')}}">Unique Identifier management</a></li>
<li class="breadcrumb-item active">Unique Identifier details {{ tag.id }}</li>
</ol>
</nav>
</div><!-- End Page Title -->
@ -26,7 +26,7 @@
<div class="row">
<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 class="row">
@ -43,16 +43,49 @@
<h5 class="card-title">Print Label</h5>
<div class="row">
<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 class="row">
<div class="col">
<div class="col qr">
<div id="{{ tag.id }}"></div>
</div>
<div class="col">
<div style="padding-top: 55px"><b class="tag">{{ tag.id }}</b></div>
<div class="col dhid">
<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>
{% 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>
@ -84,20 +117,43 @@
<span class="input-group-text">mm</span>
</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 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>

View File

@ -19,18 +19,18 @@
<div class="card-body pt-3">
<!-- 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">
<i class="bi bi-plus"></i>
Create Named Tag
Create Named Unique Identifier
<span class="caret"></span>
</a>
</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">
<i class="bi bi-plus"></i>
Create UnNamed Tag
Create UnNamed Unique Identifier
<span class="caret"></span>
</a>
</div>
@ -53,11 +53,11 @@
{% for tag in tags %}
<tr>
<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>
{% 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 }}
</a>
{% endif %}

View File

@ -5,7 +5,7 @@
<h1>Print Labels</h1>
<nav>
<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>
</ol>
</nav>
@ -24,16 +24,39 @@
<div class="row">
<div class="col-lg-3 col-md-4">
{% for tag in tags %}
<div style="width:256px; height:148px; border: solid 1px; padding: 10px;">
<div id="print">
{% for dev in devices %}
<div style="width:256px; min-height:148px; border: solid 1px; padding: 10px;">
<div>
<div class="row">
<div class="col">
<div id="{{ tag.id }}"></div>
<div class="col qr">
<div id="{{ dev.devicehub_id }}"></div>
</div>
<div class="col">
<div class="col dhid">
<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>
@ -71,20 +94,41 @@
<span class="input-group-text">mm</span>
</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 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>
@ -96,8 +140,8 @@
<script src="{{ url_for('static', filename='js/jspdf.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/print.pdf.js') }}"></script>
<script type="text/javascript">
{% for tag in tags %}
qr_draw("{{ url_for('inventory.device_details', id=tag.device.devicehub_id, _external=True) }}", "#{{ tag.id }}")
{% for dev in devices %}
qr_draw("{{ url_for('inventory.device_details', id=dev.devicehub_id, _external=True) }}", "#{{ dev.devicehub_id }}")
{% endfor %}
</script>
{% endblock main %}

View File

@ -5,7 +5,7 @@
<h1>{{ title }}</h1>
<nav>
<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>
</ol>
</nav>
@ -19,8 +19,8 @@
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Add a new Tag</h5>
<p class="text-center small">Please enter a code for the tag.</p>
<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 unique identifier.</p>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
@ -37,7 +37,7 @@
<label for="code" class="form-label">code</label>
<div class="input-group has-validation">
<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>
{% if form.code.errors %}
<p class="text-danger">

View File

@ -5,7 +5,7 @@
<h1>{{ title }}</h1>
<nav>
<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>
</ol>
</nav>
@ -19,8 +19,8 @@
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Add new Unnamed Tags</h5>
<p class="text-center small">Please enter a number of the tags to issue.</p>
<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 unique identifiers to issue.</p>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
@ -37,7 +37,7 @@
<label for="code" class="form-label">Amount</label>
<div class="input-group has-validation">
{{ 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>
{% if form.amount.errors %}
<p class="text-danger">

View File

@ -1,10 +1,14 @@
import flask
from flask import Blueprint
from flask import Blueprint, g
from flask.views import View
from flask_login import current_user, login_required, login_user, logout_user
from sqlalchemy import or_
from ereuse_devicehub import __version__
from ereuse_devicehub.forms import LoginForm
from ereuse_devicehub import __version__, messages
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.utils import is_safe_url
@ -45,18 +49,65 @@ class LogoutView(View):
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]
template_name = 'ereuse_devicehub/user_profile.html'
def dispatch_request(self):
context = {
'current_user': current_user,
'version': __version__,
}
return flask.render_template(self.template_name, **context)
self.get_context()
self.context.update(
{
'current_user': current_user,
'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('/logout/', view_func=LogoutView.as_view('logout'))
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'))

30
package.json Normal file
View File

@ -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"
}
}

View File

@ -1,29 +1,31 @@
import io
import json
import uuid
import jwt
import ereuse_utils
from contextlib import redirect_stdout
from datetime import datetime
from pathlib import Path
from decouple import config
import boltons.urlutils
import ereuse_utils
import jwt
import pytest
import yaml
from decouple import config
from psycopg2 import IntegrityError
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.db import db
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.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.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)
"""A dummy starting time to use in tests."""
@ -52,6 +54,21 @@ def config():
@pytest.fixture(scope='session')
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)
@ -63,14 +80,15 @@ def app(request, _app: Devicehub) -> Devicehub:
db.drop_all()
def _init():
_app.init_db(name='Test Inventory',
org_name='FooOrg',
org_id='foo-org-id',
tag_url=boltons.urlutils.URL('https://example.com'),
tag_token=uuid.UUID('52dacef0-6bcb-4919-bfed-f10d2c96ecee'),
erase=False,
common=True)
_app.register_blueprint(api)
_app.init_db(
name='Test Inventory',
org_name='FooOrg',
org_id='foo-org-id',
tag_url=boltons.urlutils.URL('https://example.com'),
tag_token=uuid.UUID('52dacef0-6bcb-4919-bfed-f10d2c96ecee'),
erase=False,
common=True,
)
with _app.app_context():
try:
@ -102,7 +120,9 @@ def user(app: Devicehub) -> UserClient:
with app.app_context():
password = 'foo'
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()
return client
@ -114,11 +134,34 @@ def user2(app: Devicehub) -> UserClient:
password = 'foo'
email = 'foo2@foo.com'
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()
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:
user = User(email=email, password=password)
user.individuals.add(Person(name='Timmy'))
@ -148,16 +191,13 @@ def auth_app_context(app: Devicehub):
def json_encode(dev: str) -> dict:
"""Encode json."""
data = {"type": "Snapshot"}
data['data'] = jwt.encode(dev,
P,
algorithm="HS256",
json_encoder=ereuse_utils.JSONEncoder
data['data'] = jwt.encode(
dev, P, algorithm="HS256", json_encoder=ereuse_utils.JSONEncoder
)
return data
def yaml2json(name: str) -> dict:
"""Opens and parses a YAML file from the ``files`` subdir."""
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:
"""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)

View File

@ -2,7 +2,7 @@
"timestamp": "2022-03-31T19:09:57.167164",
"type": "Snapshot",
"uuid": "cdecaf47-6e32-4ccb-b689-95c064d8c514",
"wbid": "MLKO1Y0R55XZM051WQ5KJM01RY44Q",
"sid": "MLKO1Y0R55XZM051WQ5KJM01RY44Q",
"software": "Workbench",
"version": "2022.03.00",
"schema_api": "1.0.0",

View File

@ -2,7 +2,7 @@
"timestamp": "2022-04-01 06:28:54.099394",
"type": "Snapshot",
"uuid": "232b44f3-b139-490e-90c8-2748a4523e80",
"wbid": "YKPZ27NJ2NMRO4893M4L5NRZV5YJ1",
"sid": "YKPZ27NJ2NMRO4893M4L5NRZV5YJ1",
"software": "Workbench",
"version": "2022.03.00",
"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

View File

@ -3375,5 +3375,5 @@
"type": "Snapshot",
"uuid": "b7438448-7852-42c6-ad22-0963cf7c5b43",
"version": "2022.03.3-alpha",
"wbid": "LYWP7"
"sid": "LYWP7"
}

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
import pytest
from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.client import Client
from ereuse_devicehub.devicehub import Devicehub
@pytest.mark.mvp
@ -28,38 +28,66 @@ def test_api_docs(client: Client):
"""Tests /apidocs correct initialization."""
docs, _ = client.get('/apidocs')
assert set(docs['paths'].keys()) == {
'/',
'/actions/',
'/allocates/',
'/apidocs',
'/api/inventory/',
'/allocates/',
'/deallocates/',
'/deliverynotes/',
'/devices/',
'/devices/static/{filename}',
'/documents/static/{filename}',
'/documents/actions/',
'/documents/erasures/',
'/documents/devices/',
'/documents/stamps/',
'/documents/wbconf/{wbtype}',
'/documents/internalstats/',
'/documents/stock/',
'/documents/check/',
'/documents/devices/',
'/documents/erasures/',
'/documents/internalstats/',
'/documents/lots/',
'/versions/',
'/manufacturers/',
'/documents/stamps/',
'/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/',
'/lives/',
'/login/',
'/logout/',
'/lots/',
'/lots/{id}/children',
'/lots/{id}/devices',
'/manufacturers/',
'/metrics/',
'/profile/',
'/set_password/',
'/tags/',
'/tags/{tag_id}/device/{device_id}',
'/trade-documents/',
'/users/',
'/users/login/',
'/users/logout/',
'/versions/',
}
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
assert docs['components']['securitySchemes']['bearerAuth'] == {
@ -68,6 +96,6 @@ def test_api_docs(client: Client):
'description:': 'HTTP Basic scheme',
'type': 'http',
'scheme': 'basic',
'name': 'Authorization'
'name': 'Authorization',
}
assert len(docs['definitions']) == 132

844
tests/test_render_2_0.py Normal file
View File

@ -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 &#34;Monitor&#34; 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 &#34;Recycling&#34; 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 &#34;Use&#34; 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 &#34;Refurbish&#34; 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 &#34;Management&#34; 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 &#34;Allocate&#34; 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 &#34;Deallocate&#34; 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 &#34;ToPrepare&#34; 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 &#34;Prepare&#34; 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 &#34;ToRepair&#34; 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 &#34;Ready&#34; 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 &#34;DataWipe&#34; created successfully!' in body
assert dev.devicehub_id in body

View File

@ -15,7 +15,7 @@ from requests.exceptions import HTTPError
from teal.db import DBError, UniqueViolation
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.devicehub import Devicehub
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/")
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 body['wbid'] == "LXVC"
assert ssd['serialNumber'] == 's35anx0j401001'
assert dev.manufacturer == 'lenovo'
assert body['sid'] == "MLKO1Y0R55XZM051WQ5KJM01RY44Q"
assert ssd.serial_number == 's35anx0j401001'
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
errors = SnapshotErrors.query.filter().all()
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"""
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/")
assert body['wbid'] == "YKPZ27NJ2NMRO4893M4L5NRZV5YJ1"
assert body['sid'] == "VL0L5"
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.model == 'standard'
assert dev.serial_number is None
assert dev.hid is None
assert dev.actions[0].power_on_hours == 0
assert dev.actions[1].power_on_hours == 0
assert dev.actions[0].power_on_hours == 1
assert dev.components[-1].size == 40960
assert dev.components[-1].serial_number == 'qm00001'
@pytest.mark.mvp
@ -1020,7 +1020,7 @@ def test_snapshot_wb_lite_old_snapshots(user: UserClient):
'timestamp': snapshot_11['endTime'],
'type': 'Snapshot',
'uuid': str(uuid.uuid4()),
'wbid': 'MLKO1',
'sid': 'MLKO1',
'software': 'Workbench',
'version': '2022.03.00',
"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)
bodyLite, res = user.post(snapshot_lite, uri="/api/inventory/")
dev = m.Device.query.filter_by(devicehub_id=bodyLite['dhid']).one()
components11 = []
componentsLite = []
for c in body11.get('components', []):
if c['type'] in ["HardDrive", "SolidStateDrive"]:
continue
components11.append({c.get('model'), c['type'], c.get('manufacturer')})
for c in bodyLite.get('components', []):
componentsLite.append({c.get('model'), c['type'], c.get('manufacturer')})
for c in dev.components:
componentsLite.append({c.model, c.type, c.manufacturer})
try:
assert body11['device'].get('hid') == bodyLite['device'].get('hid')
assert body11['device'].get('hid') == dev.hid
if body11['device'].get('hid'):
assert body11['device']['id'] == bodyLite['device']['id']
assert body11['device'].get('serialNumber') == bodyLite['device'].get(
'serialNumber'
)
assert body11['device'].get('model') == bodyLite['device'].get('model')
assert body11['device'].get('manufacturer') == bodyLite['device'].get(
'manufacturer'
)
assert body11['device']['id'] == dev.id
assert body11['device'].get('serialNumber') == dev.serial_number
assert body11['device'].get('model') == dev.model
assert body11['device'].get('manufacturer') == dev.manufacturer
# wbLite can find more components than wb11
assert len(components11) <= len(componentsLite)
for c in components11:
assert c in componentsLite
except Exception as err:
# import pdb; pdb.set_trace()
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.usefixtures(conftest.app_context.__name__)
def test_snapshot_errors(user: UserClient):
@ -1076,7 +1212,7 @@ def test_snapshot_errors(user: UserClient):
'timestamp': snapshot_11['endTime'],
'type': 'Snapshot',
'uuid': str(uuid.uuid4()),
'wbid': 'MLKO1',
'sid': 'MLKO1',
'software': 'Workbench',
'version': '2022.03.00',
"schema_api": "1.0.0",
@ -1093,25 +1229,22 @@ def test_snapshot_errors(user: UserClient):
body11, res = user.post(snapshot_11, res=Snapshot)
assert SnapshotErrors.query.all() == []
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 body11['device'].get('hid') == bodyLite['device'].get('hid')
assert body11['device']['id'] == bodyLite['device']['id']
assert body11['device'].get('serialNumber') == bodyLite['device'].get(
'serialNumber'
)
assert body11['device'].get('model') == bodyLite['device'].get('model')
assert body11['device'].get('manufacturer') == bodyLite['device'].get(
'manufacturer'
)
assert body11['device'].get('hid') == dev.hid
assert body11['device']['id'] == dev.id
assert body11['device'].get('serialNumber') == dev.serial_number
assert body11['device'].get('model') == dev.model
assert body11['device'].get('manufacturer') == dev.manufacturer
components11 = []
componentsLite = []
for c in body11['components']:
if c['type'] == "HardDrive":
continue
components11.append({c['model'], c['type'], c['manufacturer']})
for c in bodyLite['components']:
componentsLite.append({c['model'], c['type'], c['manufacturer']})
for c in dev.components:
componentsLite.append({c.model, c.type, c.manufacturer})
assert len(components11) == len(componentsLite)
for c in components11:
@ -1123,10 +1256,39 @@ def test_snapshot_errors(user: UserClient):
def test_snapshot_errors_timestamp(user: UserClient):
"""This test check the minimum validation of json that come from snapshot"""
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/")
assert res.status_code == 201
assert len(SnapshotErrors.query.all()) == 1
error = SnapshotErrors.query.all()[0]
assert snapshot_lite['wbid'] == error.wbid
assert user.user['id'] == str(error.owner_id)
assert len(SnapshotErrors.query.all()) == 0
dev = m.Device.query.filter_by(devicehub_id=bodyLite['dhid']).one()
assert not dev.model
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()