diff --git a/ereuse_devicehub/client.py b/ereuse_devicehub/client.py index 3224dfd3..aaafb9f3 100644 --- a/ereuse_devicehub/client.py +++ b/ereuse_devicehub/client.py @@ -2,7 +2,10 @@ from inspect import isclass from typing import Dict, Iterable, Type, Union from ereuse_utils.test import JSON, Res -from teal.client import Client as TealClient, Query, Status +from flask.testing import FlaskClient +from flask_wtf.csrf import generate_csrf +from teal.client import Client as TealClient +from teal.client import Query, Status from werkzeug.exceptions import HTTPException from ereuse_devicehub.resources import models, schemas @@ -13,110 +16,156 @@ ResourceLike = Union[Type[Union[models.Thing, schemas.Thing]], str] class Client(TealClient): """A client suited for Devicehub main usage.""" - def __init__(self, application, - response_wrapper=None, - use_cookies=False, - allow_subdomain_redirects=False): - super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects) + def __init__( + self, + application, + response_wrapper=None, + use_cookies=False, + allow_subdomain_redirects=False, + ): + super().__init__( + application, response_wrapper, use_cookies, allow_subdomain_redirects + ) - def open(self, - uri: str, - res: ResourceLike = None, - status: Status = 200, - query: Query = tuple(), - accept=JSON, - content_type=JSON, - item=None, - headers: dict = None, - token: str = None, - **kw) -> Res: + def open( + self, + uri: str, + res: ResourceLike = None, + status: Status = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + item=None, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)): res = res.t - return super().open(uri, res, status, query, accept, content_type, item, headers, token, - **kw) + return super().open( + uri, res, status, query, accept, content_type, item, headers, token, **kw + ) - def get(self, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - status: Status = 200, - item: Union[int, str] = None, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: + def get( + self, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + status: Status = 200, + item: Union[int, str] = None, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: return super().get(uri, res, query, status, item, accept, headers, token, **kw) - def post(self, - data: str or dict, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - status: Status = 201, - content_type: str = JSON, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().post(data, uri, res, query, status, content_type, accept, headers, token, - **kw) + def post( + self, + data: str or dict, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + status: Status = 201, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().post( + data, uri, res, query, status, content_type, accept, headers, token, **kw + ) - def patch(self, - data: str or dict, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - item: Union[int, str] = None, - status: Status = 200, - content_type: str = JSON, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().patch(data, uri, res, query, item, status, content_type, accept, token, - headers, **kw) + def patch( + self, + data: str or dict, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + item: Union[int, str] = None, + status: Status = 200, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().patch( + data, + uri, + res, + query, + item, + status, + content_type, + accept, + token, + headers, + **kw, + ) - def put(self, - data: str or dict, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - item: Union[int, str] = None, - status: Status = 201, - content_type: str = JSON, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().put(data, uri, res, query, item, status, content_type, accept, token, - headers, **kw) + def put( + self, + data: str or dict, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + item: Union[int, str] = None, + status: Status = 201, + content_type: str = JSON, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().put( + data, + uri, + res, + query, + item, + status, + content_type, + accept, + token, + headers, + **kw, + ) - def delete(self, - uri: str = '', - res: ResourceLike = None, - query: Query = tuple(), - status: Status = 204, - item: Union[int, str] = None, - accept: str = JSON, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().delete(uri, res, query, status, item, accept, headers, token, **kw) + def delete( + self, + uri: str = '', + res: ResourceLike = None, + query: Query = tuple(), + status: Status = 204, + item: Union[int, str] = None, + accept: str = JSON, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().delete( + uri, res, query, status, item, accept, headers, token, **kw + ) def login(self, email: str, password: str): assert isinstance(email, str) assert isinstance(password, str) - return self.post({'email': email, 'password': password}, '/users/login/', status=200) + return self.post( + {'email': email, 'password': password}, '/users/login/', status=200 + ) - def get_many(self, - res: ResourceLike, - resources: Iterable[Union[dict, int]], - key: str = None, - **kw) -> Iterable[Union[Dict[str, object], str]]: + def get_many( + self, + res: ResourceLike, + resources: Iterable[Union[dict, int]], + key: str = None, + **kw, + ) -> Iterable[Union[Dict[str, object], str]]: """Like :meth:`.get` but with many resources.""" return ( - self.get(res=res, item=r[key] if key else r, **kw)[0] - for r in resources + self.get(res=res, item=r[key] if key else r, **kw)[0] for r in resources ) @@ -126,33 +175,119 @@ class UserClient(Client): It will automatically perform login on the first request. """ - def __init__(self, application, - email: str, - password: str, - response_wrapper=None, - use_cookies=False, - allow_subdomain_redirects=False): - super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects) + def __init__( + self, + application, + email: str, + password: str, + response_wrapper=None, + use_cookies=False, + allow_subdomain_redirects=False, + ): + super().__init__( + application, response_wrapper, use_cookies, allow_subdomain_redirects + ) self.email = email # type: str self.password = password # type: str self.user = None # type: dict - def open(self, - uri: str, - res: ResourceLike = None, - status: int or HTTPException = 200, - query: Query = tuple(), - accept=JSON, - content_type=JSON, - item=None, - headers: dict = None, - token: str = None, - **kw) -> Res: - return super().open(uri, res, status, query, accept, content_type, item, headers, - self.user['token'] if self.user else token, **kw) + def open( + self, + uri: str, + res: ResourceLike = None, + status: int or HTTPException = 200, + query: Query = tuple(), + accept=JSON, + content_type=JSON, + item=None, + headers: dict = None, + token: str = None, + **kw, + ) -> Res: + return super().open( + uri, + res, + status, + query, + accept, + content_type, + item, + headers, + self.user['token'] if self.user else token, + **kw, + ) # noinspection PyMethodOverriding def login(self): response = super().login(self.email, self.password) self.user = response[0] return response + + +class UserClientFlask: + def __init__( + self, + application, + email: str, + password: str, + response_wrapper=None, + use_cookies=True, + follow_redirects=True, + ): + self.email = email + self.password = password + self.follow_redirects = follow_redirects + self.user = None + + self.client = FlaskClient(application, use_cookies=use_cookies) + self.client.get('/login/') + + data = { + 'email': email, + 'password': password, + 'csrf_token': generate_csrf(), + } + body, status, headers = self.client.post( + '/login/', data=data, follow_redirects=True + ) + self.headers = headers + body = next(body).decode("utf-8") + assert "Unassgined" in body + + def get( + self, + uri='', + data=None, + follow_redirects=True, + content_type='text/html; charset=utf-8', + decode=True, + **kw, + ): + + body, status, headers = self.client.get( + uri, data=data, follow_redirects=follow_redirects, headers=self.headers + ) + if decode: + body = next(body).decode("utf-8") + return (body, status) + + def post( + self, + uri='', + data=None, + follow_redirects=True, + content_type='application/x-www-form-urlencoded', + decode=True, + **kw, + ): + + body, status, headers = self.client.post( + uri, + data=data, + follow_redirects=follow_redirects, + headers=self.headers, + content_type=content_type, + ) + if decode: + body = next(body).decode("utf-8") + return (body, status) diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 72f033f8..08b6eeca 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -497,7 +497,7 @@ class TagDeviceForm(FlaskForm): db.session.commit() -class NewActionForm(FlaskForm): +class ActionFormMix(FlaskForm): name = StringField( 'Name', [validators.length(max=50)], @@ -529,17 +529,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 @@ -572,7 +578,20 @@ class NewActionForm(FlaskForm): return self.type.data -class AllocateForm(NewActionForm): +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') final_user_code = StringField('Final user code', [validators.length(max=50)]) @@ -582,6 +601,9 @@ class AllocateForm(NewActionForm): def validate(self, extra_validators=None): is_valid = super().validate(extra_validators) + if self.type.data not in ['Allocate', 'Deallocate']: + return False + start_time = self.start_time.data end_time = self.end_time.data if start_time and end_time and end_time < start_time: @@ -650,7 +672,7 @@ class DataWipeDocumentForm(Form): return self._obj -class DataWipeForm(NewActionForm): +class DataWipeForm(ActionFormMix): document = FormField(DataWipeDocumentForm) def save(self): @@ -677,7 +699,7 @@ class DataWipeForm(NewActionForm): return self.instance -class TradeForm(NewActionForm): +class TradeForm(ActionFormMix): user_from = StringField( 'Supplier', [validators.Optional()], @@ -724,6 +746,9 @@ class TradeForm(NewActionForm): email_from = self.user_from.data email_to = self.user_to.data + if self.type.data != "Trade": + return False + if not self.confirm.data and not self.code.data: self.code.errors = ["If you don't want to confirm, you need a code"] is_valid = False diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 38ff4bc6..503632e6 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -7,10 +7,9 @@ import flask_weasyprint from flask import Blueprint, g, make_response, request, url_for from flask.views import View from flask_login import current_user, login_required -from sqlalchemy import or_ from werkzeug.exceptions import NotFound -from ereuse_devicehub import __version__, messages +from ereuse_devicehub import messages from ereuse_devicehub.db import db from ereuse_devicehub.inventory.forms import ( AllocateForm, @@ -31,35 +30,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): + super().get_context() + lots = self.context['lots'] form_filter = FilterForm() filter_types = form_filter.search() - lots = self.get_lots() lot = None tags = ( Tag.query.filter(Tag.owner_id == current_user.id) @@ -105,21 +90,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 @@ -135,20 +120,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): @@ -164,17 +149,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' @@ -187,14 +172,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): @@ -221,24 +206,25 @@ 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 = form.save(commit=False) if lot_id: + lots = self.context['lots'] lot = lots.filter(Lot.id == lot_id).one() lot.devices.add(snapshot.device) 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): @@ -247,20 +233,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) @@ -269,7 +256,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): @@ -285,13 +272,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() @@ -299,14 +286,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'] @@ -315,16 +303,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 @@ -350,10 +341,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): @@ -372,10 +365,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): @@ -394,10 +386,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): @@ -409,6 +400,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() @@ -416,9 +408,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): diff --git a/ereuse_devicehub/labels/forms.py b/ereuse_devicehub/labels/forms.py index cd4b5bec..98427215 100644 --- a/ereuse_devicehub/labels/forms.py +++ b/ereuse_devicehub/labels/forms.py @@ -64,10 +64,7 @@ class PrintLabelsForm(FlaskForm): .all() ) - # print only tags that are DHID - dhids = [x.devicehub_id for x in self._devices] - self._tags = ( - Tag.query.filter(Tag.owner_id == g.user.id).filter(Tag.id.in_(dhids)).all() - ) + if not self._devices: + return False return is_valid diff --git a/ereuse_devicehub/labels/views.py b/ereuse_devicehub/labels/views.py index 445a4eb8..e7fc3b0d 100644 --- a/ereuse_devicehub/labels/views.py +++ b/ereuse_devicehub/labels/views.py @@ -27,7 +27,7 @@ class TagListView(View): context = { 'lots': lots, 'tags': tags, - 'page_title': 'Tags Management', + 'page_title': 'Unique Identifiers Management', 'version': __version__, } return flask.render_template(self.template_name, **context) @@ -102,7 +102,7 @@ class PrintLabelsView(View): form = PrintLabelsForm() if form.validate_on_submit(): context['form'] = form - context['tags'] = form._tags + context['devices'] = form._devices return flask.render_template(self.template_name, **context) else: messages.error('Error you need select one or more devices') diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 98227c4c..09451277 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -1,20 +1,30 @@ -import pathlib import copy +import pathlib import time -from flask import g from contextlib import suppress from fractions import Fraction from itertools import chain from operator import attrgetter from typing import Dict, List, Set -from flask_sqlalchemy import event from boltons import urlutils from citext import CIText from ereuse_utils.naming import HID_CONVERSION_DOC, Naming +from flask import g +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]: @@ -260,6 +307,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 +316,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 +337,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 +351,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 +381,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 +462,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 +472,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 +481,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 +490,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 +528,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 +557,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 +572,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 +611,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 +662,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 +718,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 +745,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 +753,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) def __init__(self, *args, **kwargs) -> None: @@ -684,22 +787,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]: @@ -724,16 +835,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) @@ -756,8 +869,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: @@ -823,9 +939,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') @@ -855,21 +975,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: @@ -881,11 +1004,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 @@ -908,7 +1036,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)) @@ -919,6 +1048,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: @@ -933,9 +1063,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() @@ -985,6 +1115,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)) @@ -999,6 +1130,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)) @@ -1016,6 +1148,7 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component): mobiles, smart-watches, and so on; excluding ``ComputerMonitor`` and ``TelevisionSet``. """ + pass @@ -1031,14 +1164,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) @@ -1051,6 +1186,7 @@ class Camera(Component): class ComputerAccessory(Device): """Computer peripherals and similar accessories.""" + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) pass @@ -1073,6 +1209,7 @@ class MemoryCardReader(ComputerAccessory): class Networking(NetworkMixin, Device): """Routers, switches, hubs...""" + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) @@ -1118,6 +1255,7 @@ class Microphone(Sound): class Video(Device): """Devices related to video treatment.""" + pass @@ -1131,6 +1269,7 @@ class Videoconference(Video): class Cooking(Device): """Cooking devices.""" + pass @@ -1182,6 +1321,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) @@ -1192,7 +1332,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 @@ -1202,10 +1342,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) @@ -1217,6 +1354,7 @@ def create_code_tag(mapper, connection, device): this tag is the same of devicehub_id. """ from ereuse_devicehub.resources.tag.model import Tag + if isinstance(device, Computer): tag = Tag(device_id=device.id, id=device.devicehub_id) db.session.add(tag) diff --git a/ereuse_devicehub/static/js/print.pdf.js b/ereuse_devicehub/static/js/print.pdf.js index 0d6fe6d5..f0b1817c 100644 --- a/ereuse_devicehub/static/js/print.pdf.js +++ b/ereuse_devicehub/static/js/print.pdf.js @@ -1,8 +1,10 @@ $(document).ready(function() { STORAGE_KEY = 'tag-spec-key'; $("#printerType").on("change", change_size); + $(".form-check-input").on("change", change_check); change_size(); - load_size(); + load_settings(); + change_check(); }) function qr_draw(url, id) { @@ -16,27 +18,43 @@ function qr_draw(url, id) { }); } -function save_size() { +function save_settings() { var height = $("#height-tag").val(); var width = $("#width-tag").val(); var sizePreset = $("#printerType").val(); var data = {"height": height, "width": width, "sizePreset": sizePreset}; + data['dhid'] = $("#dhidCheck").prop('checked'); + data['qr'] = $("#qrCheck").prop('checked'); + data['serial_number'] = $("#serialNumberCheck").prop('checked'); + data['manufacturer'] = $("#manufacturerCheck").prop('checked'); + data['model'] = $("#modelCheck").prop('checked'); localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } -function load_size() { +function load_settings() { var data = JSON.parse(localStorage.getItem(STORAGE_KEY)); if (data){ $("#height-tag").val(data.height); $("#width-tag").val(data.width); $("#printerType").val(data.sizePreset); + $("#qrCheck").prop('checked', data.qr); + $("#dhidCheck").prop('checked', data.dhid); + $("#serialNumberCheck").prop('checked', data.serial_number); + $("#manufacturerCheck").prop('checked', data.manufacturer); + $("#modelCheck").prop('checked', data.model); }; } -function reset_size() { +function reset_settings() { localStorage.removeItem(STORAGE_KEY); $("#printerType").val('brotherSmall'); + $("#qrCheck").prop('checked', true); + $("#dhidCheck").prop('checked', true); + $("#serialNumberCheck").prop('checked', false); + $("#manufacturerCheck").prop('checked', false); + $("#modelCheck").prop('checked', false); change_size(); + change_check(); } function change_size() { @@ -50,29 +68,101 @@ function change_size() { } } +function change_check() { + if ($("#dhidCheck").prop('checked')) { + $(".dhid").show(); + } else { + $(".dhid").hide(); + } + if ($("#serialNumberCheck").prop('checked')) { + $(".serial_number").show(); + } else { + $(".serial_number").hide(); + } + if ($("#manufacturerCheck").prop('checked')) { + $(".manufacturer").show(); + } else { + $(".manufacturer").hide(); + } + if ($("#modelCheck").prop('checked')) { + $(".model").show(); + } else { + $(".model").hide(); + } + if ($("#qrCheck").prop('checked')) { + $(".qr").show(); + } else { + $(".qr").hide(); + } +} + function printpdf() { var border = 2; + var line = 5; var height = parseInt($("#height-tag").val()); var width = parseInt($("#width-tag").val()); - img_side = Math.min(height, width) - 2*border; + var img_side = Math.min(height, width) - 2*border; max_tag_side = (Math.max(height, width)/2) + border; if (max_tag_side < img_side) { - max_tag_side = img_side+ 2*border; + max_tag_side = img_side + 2*border; }; min_tag_side = (Math.min(height, width)/2) + border; var last_tag_code = ''; + if ($("#serialNumberCheck").prop('checked')) { + height += line; + }; + if ($("#manufacturerCheck").prop('checked')) { + height += line; + }; + if ($("#modelCheck").prop('checked')) { + height += line; + }; + var pdf = new jsPDF('l', 'mm', [width, height]); $(".tag").map(function(x, y) { if (x != 0){ pdf.addPage(); - console.log(x) }; + var space = line + border; + if ($("#qrCheck").prop('checked')) { + space += img_side; + } var tag = $(y).text(); last_tag_code = tag; - var imgData = $('#'+tag+' img').attr("src"); - pdf.addImage(imgData, 'PNG', border, border, img_side, img_side); - pdf.text(tag, max_tag_side, min_tag_side); + if ($("#qrCheck").prop('checked')) { + var imgData = $('#'+tag+' img').attr("src"); + pdf.addImage(imgData, 'PNG', border, border, img_side, img_side); + }; + + if ($("#dhidCheck").prop('checked')) { + if ($("#qrCheck").prop('checked')) { + pdf.setFontSize(15); + pdf.text(tag, max_tag_side, min_tag_side); + } else { + pdf.setFontSize(15); + pdf.text(tag, border, space); + space += line; + } + }; + if ($("#serialNumberCheck").prop('checked')) { + var sn = $(y).data('serial-number'); + pdf.setFontSize(12); + pdf.text(sn, border, space); + space += line; + }; + if ($("#manufacturerCheck").prop('checked')) { + var sn = $(y).data('manufacturer'); + pdf.setFontSize(12); + pdf.text(sn, border, space); + space += line; + }; + if ($("#modelCheck").prop('checked')) { + var sn = $(y).data('model'); + pdf.setFontSize(8); + pdf.text(sn, border, space); + space += line; + }; }); pdf.save('Tag_'+last_tag_code+'.pdf'); diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index 4e721730..f10172e5 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -191,15 +191,6 @@ - - - - @@ -238,4 +229,4 @@ -{% endblock body %} \ No newline at end of file +{% endblock body %} diff --git a/ereuse_devicehub/templates/inventory/addDevicestag.html b/ereuse_devicehub/templates/inventory/addDevicestag.html index 51938211..bb02c221 100644 --- a/ereuse_devicehub/templates/inventory/addDevicestag.html +++ b/ereuse_devicehub/templates/inventory/addDevicestag.html @@ -3,14 +3,14 @@ + +