diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index d551bb0d..62f295d0 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -4,35 +4,39 @@ from json.decoder import JSONDecodeError from requests.exceptions import ConnectionError from boltons.urlutils import URL +from flask import g, request +from flask_wtf import FlaskForm +from sqlalchemy.util import OrderedSet +from wtforms import ( + BooleanField, DateField, FileField, FloatField, Form, HiddenField, + IntegerField, MultipleFileField, SelectField, StringField, TextAreaField, + URLField, validators) +from wtforms.fields import FormField + from ereuse_devicehub.db import db from ereuse_devicehub.resources.action.models import RateComputer, Snapshot from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate from ereuse_devicehub.resources.action.schemas import \ Snapshot as SnapshotSchema -from ereuse_devicehub.resources.action.views.snapshot import move_json, save_json -from ereuse_devicehub.resources.device.models import (SAI, Cellphone, Computer, - Device, Keyboard, MemoryCardReader, - Monitor, Mouse, Smartphone, Tablet) -from flask import g, request -from flask_wtf import FlaskForm -from sqlalchemy.util import OrderedSet -from wtforms import (BooleanField, DateField, FileField, FloatField, Form, - HiddenField, IntegerField, MultipleFileField, SelectField, - StringField, TextAreaField, URLField, validators) -from wtforms.fields import FormField - +from ereuse_devicehub.resources.action.views.snapshot import ( + move_json, save_json) +from ereuse_devicehub.resources.device.models import ( + SAI, Cellphone, Computer, Device, Keyboard, MemoryCardReader, Monitor, + Mouse, Smartphone, Tablet) from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.documents.models import DataWipeDocument from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware 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.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.user.exceptions import InsufficientPermission +from ereuse_devicehub.resources.user.models import User class LotDeviceForm(FlaskForm): - lot = StringField(u'Lot', [validators.UUID()]) - devices = StringField(u'Devices', [validators.length(min=1)]) + lot = StringField('Lot', [validators.UUID()]) + devices = StringField('Devices', [validators.length(min=1)]) def validate(self, extra_validators=None): is_valid = super().validate(extra_validators) @@ -40,16 +44,29 @@ class LotDeviceForm(FlaskForm): if not is_valid: return False - self._lot = Lot.query.filter(Lot.id == self.lot.data).filter( - Lot.owner_id == g.user.id).one() + self._lot = ( + Lot.query.filter(Lot.id == self.lot.data) + .filter(Lot.owner_id == g.user.id) + .one() + ) devices = set(self.devices.data.split(",")) - self._devices = Device.query.filter(Device.id.in_(devices)).filter( - Device.owner_id == g.user.id).distinct().all() + self._devices = ( + Device.query.filter(Device.id.in_(devices)) + .filter(Device.owner_id == g.user.id) + .distinct() + .all() + ) return bool(self._devices) def save(self): + trade = self._lot.trade + if trade: + for dev in self._devices: + if trade not in dev.actions: + trade.devices.add(dev) + self._lot.devices.update(self._devices) db.session.add(self._lot) db.session.commit() @@ -61,14 +78,17 @@ class LotDeviceForm(FlaskForm): class LotForm(FlaskForm): - name = StringField(u'Name', [validators.length(min=1)]) + name = StringField('Name', [validators.length(min=1)]) def __init__(self, *args, **kwargs): self.id = kwargs.pop('id', None) self.instance = None if self.id: - self.instance = Lot.query.filter(Lot.id == self.id).filter( - Lot.owner_id == g.user.id).one() + self.instance = ( + Lot.query.filter(Lot.id == self.id) + .filter(Lot.owner_id == g.user.id) + .one() + ) super().__init__(*args, **kwargs) if self.instance and not self.name.data: self.name.data = self.instance.name @@ -96,7 +116,7 @@ class LotForm(FlaskForm): class UploadSnapshotForm(FlaskForm): - snapshot = MultipleFileField(u'Select a Snapshot File', [validators.DataRequired()]) + snapshot = MultipleFileField('Select a Snapshot File', [validators.DataRequired()]) def validate(self, extra_validators=None): is_valid = super().validate(extra_validators) @@ -164,8 +184,10 @@ class UploadSnapshotForm(FlaskForm): # this is a copy adaptated from ereuse_devicehub.resources.action.views.snapshot device = snapshot_json.pop('device') # type: Computer components = None - if snapshot_json['software'] == (SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid): - components = snapshot_json.pop('components', None) # type: List[Component] + if snapshot_json['software'] == ( + SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid + ): + components = snapshot_json.pop('components', None) if isinstance(device, Computer) and device.hid: device.add_mac_to_hid(components_snap=components) snapshot = Snapshot(**snapshot_json) @@ -174,7 +196,9 @@ class UploadSnapshotForm(FlaskForm): actions_device = set(e for e in device.actions_one) device.actions_one.clear() if components: - actions_components = tuple(set(e for e in c.actions_one) for c in components) + actions_components = tuple( + set(e for e in c.actions_one) for c in components + ) for component in components: component.actions_one.clear() @@ -220,38 +244,40 @@ class UploadSnapshotForm(FlaskForm): class NewDeviceForm(FlaskForm): - type = StringField(u'Type', [validators.DataRequired()]) - label = StringField(u'Label') - serial_number = StringField(u'Seria Number', [validators.DataRequired()]) - model = StringField(u'Model', [validators.DataRequired()]) - manufacturer = StringField(u'Manufacturer', [validators.DataRequired()]) - appearance = StringField(u'Appearance', [validators.Optional()]) - functionality = StringField(u'Functionality', [validators.Optional()]) - brand = StringField(u'Brand') - generation = IntegerField(u'Generation') - version = StringField(u'Version') - weight = FloatField(u'Weight', [validators.DataRequired()]) - width = FloatField(u'Width', [validators.DataRequired()]) - height = FloatField(u'Height', [validators.DataRequired()]) - depth = FloatField(u'Depth', [validators.DataRequired()]) - variant = StringField(u'Variant', [validators.Optional()]) - sku = StringField(u'SKU', [validators.Optional()]) - image = StringField(u'Image', [validators.Optional(), validators.URL()]) - imei = IntegerField(u'IMEI', [validators.Optional()]) - meid = StringField(u'MEID', [validators.Optional()]) - resolution = IntegerField(u'Resolution width', [validators.Optional()]) - screen = FloatField(u'Screen size', [validators.Optional()]) + type = StringField('Type', [validators.DataRequired()]) + label = StringField('Label') + serial_number = StringField('Seria Number', [validators.DataRequired()]) + model = StringField('Model', [validators.DataRequired()]) + manufacturer = StringField('Manufacturer', [validators.DataRequired()]) + appearance = StringField('Appearance', [validators.Optional()]) + functionality = StringField('Functionality', [validators.Optional()]) + brand = StringField('Brand') + generation = IntegerField('Generation') + version = StringField('Version') + weight = FloatField('Weight', [validators.DataRequired()]) + width = FloatField('Width', [validators.DataRequired()]) + height = FloatField('Height', [validators.DataRequired()]) + depth = FloatField('Depth', [validators.DataRequired()]) + variant = StringField('Variant', [validators.Optional()]) + sku = StringField('SKU', [validators.Optional()]) + image = StringField('Image', [validators.Optional(), validators.URL()]) + imei = IntegerField('IMEI', [validators.Optional()]) + meid = StringField('MEID', [validators.Optional()]) + resolution = IntegerField('Resolution width', [validators.Optional()]) + screen = FloatField('Screen size', [validators.Optional()]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.devices = {"Smartphone": Smartphone, - "Tablet": Tablet, - "Cellphone": Cellphone, - "Monitor": Monitor, - "Mouse": Mouse, - "Keyboard": Keyboard, - "SAI": SAI, - "MemoryCardReader": MemoryCardReader} + self.devices = { + "Smartphone": Smartphone, + "Tablet": Tablet, + "Cellphone": Cellphone, + "Monitor": Monitor, + "Mouse": Mouse, + "Keyboard": Keyboard, + "SAI": SAI, + "MemoryCardReader": MemoryCardReader, + } if not self.generation.data: self.generation.data = 1 @@ -328,29 +354,31 @@ class NewDeviceForm(FlaskForm): 'software': 'Web', 'version': '11.0', 'device': { - 'type': self.type.data, - 'model': self.model.data, - 'manufacturer': self.manufacturer.data, - 'serialNumber': self.serial_number.data, - 'brand': self.brand.data, - 'version': self.version.data, - 'generation': self.generation.data, - 'sku': self.sku.data, - 'weight': self.weight.data, - 'width': self.width.data, - 'height': self.height.data, - 'depth': self.depth.data, - 'variant': self.variant.data, - 'image': self.image.data - } + 'type': self.type.data, + 'model': self.model.data, + 'manufacturer': self.manufacturer.data, + 'serialNumber': self.serial_number.data, + 'brand': self.brand.data, + 'version': self.version.data, + 'generation': self.generation.data, + 'sku': self.sku.data, + 'weight': self.weight.data, + 'width': self.width.data, + 'height': self.height.data, + 'depth': self.depth.data, + 'variant': self.variant.data, + 'image': self.image.data, + }, } if self.appearance.data or self.functionality.data: - json_snapshot['device']['actions'] = [{ - 'type': 'VisualTest', - 'appearanceRange': self.appearance.data, - 'functionalityRange': self.functionality.data - }] + json_snapshot['device']['actions'] = [ + { + 'type': 'VisualTest', + 'appearanceRange': self.appearance.data, + 'functionalityRange': self.functionality.data, + } + ] upload_form = UploadSnapshotForm() upload_form.sync = Sync() @@ -380,14 +408,14 @@ class NewDeviceForm(FlaskForm): class TagForm(FlaskForm): - code = StringField(u'Code', [validators.length(min=1)]) + code = StringField('Code', [validators.length(min=1)]) def validate(self, extra_validators=None): error = ["This value is being used"] is_valid = super().validate(extra_validators) if not is_valid: return False - tag = Tag.query.filter(Tag.id==self.code.data).all() + tag = Tag.query.filter(Tag.id == self.code.data).all() if tag: self.code.errors = error return False @@ -408,7 +436,7 @@ class TagForm(FlaskForm): class TagUnnamedForm(FlaskForm): - amount = IntegerField(u'amount') + amount = IntegerField('amount') def save(self): num = self.amount.data @@ -424,8 +452,8 @@ class TagUnnamedForm(FlaskForm): class TagDeviceForm(FlaskForm): - tag = SelectField(u'Tag', choices=[]) - device = StringField(u'Device', [validators.Optional()]) + tag = SelectField('Tag', choices=[]) + device = StringField('Device', [validators.Optional()]) def __init__(self, *args, **kwargs): self.delete = kwargs.pop('delete', None) @@ -434,9 +462,11 @@ class TagDeviceForm(FlaskForm): super().__init__(*args, **kwargs) if self.delete: - tags = Tag.query.filter(Tag.owner_id==g.user.id).filter(Tag.device_id==self.device_id) + tags = Tag.query.filter(Tag.owner_id == g.user.id).filter_by( + device_id=self.device_id + ) else: - tags = Tag.query.filter(Tag.owner_id==g.user.id).filter(Tag.device_id==None) + tags = Tag.query.filter(Tag.owner_id == g.user.id).filter_by(device_id=None) self.tag.choices = [(tag.id, tag.id) for tag in tags] @@ -446,8 +476,11 @@ class TagDeviceForm(FlaskForm): if not is_valid: return False - self._tag = Tag.query.filter(Tag.id == self.tag.data).filter( - Tag.owner_id == g.user.id).one() + self._tag = ( + Tag.query.filter(Tag.id == self.tag.data) + .filter(Tag.owner_id == g.user.id) + .one() + ) if not self.delete and self._tag.device_id: self.tag.errors = [("This tag is actualy in use.")] @@ -461,8 +494,11 @@ class TagDeviceForm(FlaskForm): if self.device_id or self.device.data: self.device_id = self.device_id or self.device.data - self._device = Device.query.filter(Device.id == self.device_id).filter( - Device.owner_id == g.user.id).one() + self._device = ( + Device.query.filter(Device.id == self.device_id) + .filter(Device.owner_id == g.user.id) + .one() + ) return True @@ -478,38 +514,55 @@ class TagDeviceForm(FlaskForm): class NewActionForm(FlaskForm): - name = StringField(u'Name', [validators.length(max=50)], - description="A name or title of the event. Something to look for.") + name = StringField( + 'Name', + [validators.length(max=50)], + description="A name or title of the event. Something to look for.", + ) devices = HiddenField() - date = DateField(u'Date', [validators.Optional()], - description="""When the action ends. For some actions like booking + date = DateField( + 'Date', + [validators.Optional()], + description="""When the action ends. For some actions like booking the time when it expires, for others like renting the time that the end rents. For specific actions, it is the time in which they are carried out; differs from created - in that created is where the system receives the action.""") - severity = SelectField(u'Severity', choices=[(v.name, v.name) for v in Severity], - description="""An indicator that evaluates the execution of the event. - For example, failed events are set to Error""") - description = TextAreaField(u'Description') + in that created is where the system receives the action.""", + ) + severity = SelectField( + 'Severity', + choices=[(v.name, v.name) for v in Severity], + description="""An indicator that evaluates the execution of the event. + For example, failed events are set to Error""", + ) + description = TextAreaField('Description') lot = HiddenField() type = HiddenField() def validate(self, extra_validators=None): - is_valid = super().validate(extra_validators) + is_valid = self.generic_validation(extra_validators=extra_validators) 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()) + 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 + def generic_validation(self, extra_validators=None): + # Some times we want check validations without devices list + return super().validate(extra_validators) + def save(self): Model = db.Model._decl_class_registry.data[self.type.data]() self.instance = Model() @@ -527,13 +580,20 @@ class NewActionForm(FlaskForm): return self.instance + def check_valid(self): + if self.type.data in ['', None]: + return + + if not self.validate(): + return self.type.data + class AllocateForm(NewActionForm): - start_time = DateField(u'Start time') - end_time = DateField(u'End time') - final_user_code = StringField(u'Final user code', [validators.length(max=50)]) - transaction = StringField(u'Transaction', [validators.length(max=50)]) - end_users = IntegerField(u'End users') + start_time = DateField('Start time') + end_time = DateField('End time') + final_user_code = StringField('Final user code', [validators.length(max=50)]) + transaction = StringField('Transaction', [validators.length(max=50)]) + end_users = IntegerField('End users') def validate(self, extra_validators=None): is_valid = super().validate(extra_validators) @@ -554,19 +614,31 @@ class AllocateForm(NewActionForm): class DataWipeDocumentForm(Form): - date = DateField(u'Date', [validators.Optional()], - description="Date when was data wipe") - url = URLField(u'Url', [validators.Optional()], - description="Url where the document resides") - success = BooleanField(u'Success', [validators.Optional()], - description="The erase was success or not?") - software = StringField(u'Software', [validators.Optional()], - description="Which software has you use for erase the disks") - id_document = StringField(u'Document Id', [validators.Optional()], - description="Identification number of document") - file_name = FileField(u'File', [validators.DataRequired()], - description="""This file is not stored on our servers, it is only used to - generate a digital signature and obtain the name of the file.""") + date = DateField( + 'Date', [validators.Optional()], description="Date when was data wipe" + ) + url = URLField( + 'Url', [validators.Optional()], description="Url where the document resides" + ) + success = BooleanField( + 'Success', [validators.Optional()], description="The erase was success or not?" + ) + software = StringField( + 'Software', + [validators.Optional()], + description="Which software has you use for erase the disks", + ) + id_document = StringField( + 'Document Id', + [validators.Optional()], + description="Identification number of document", + ) + file_name = FileField( + 'File', + [validators.DataRequired()], + description="""This file is not stored on our servers, it is only used to + generate a digital signature and obtain the name of the file.""", + ) def validate(self, extra_validators=None): is_valid = super().validate(extra_validators) @@ -619,3 +691,230 @@ class DataWipeForm(NewActionForm): self.document = document return self.instance + + +class TradeForm(NewActionForm): + user_from = StringField( + 'Supplier', + [validators.Optional()], + description="Please enter the supplier's email address", + render_kw={'data-email': ""}, + ) + user_to = StringField( + 'Receiver', + [validators.Optional()], + description="Please enter the receiver's email address", + render_kw={'data-email': ""}, + ) + confirm = BooleanField( + 'Confirm', + [validators.Optional()], + default=True, + description="I need confirmation from the other user for every device and document.", + ) + code = StringField( + 'Code', + [validators.Optional()], + description="If you don't need confirm, you need put a code for trace the user in the statistics.", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user_from.render_kw['data-email'] = g.user.email + self.user_to.render_kw['data-email'] = g.user.email + self._lot = ( + Lot.query.filter(Lot.id == self.lot.data) + .filter(Lot.owner_id == g.user.id) + .one() + ) + + def validate(self, extra_validators=None): + is_valid = self.generic_validation(extra_validators=extra_validators) + email_from = self.user_from.data + email_to = self.user_to.data + + if not self.confirm.data and not self.code.data: + self.code.errors = ["If you don't want to confirm, you need a code"] + is_valid = False + + if ( + self.confirm.data + and not (email_from and email_to) + or email_to == email_from + or g.user.email not in [email_from, email_to] + ): + + errors = ["If you want confirm, you need a correct email"] + self.user_to.errors = errors + self.user_from.errors = errors + + is_valid = False + + if self.confirm.data and is_valid: + user_to = User.query.filter_by(email=email_to).first() or g.user + user_from = User.query.filter_by(email=email_from).first() or g.user + if user_to == user_from: + is_valid = False + else: + self.db_user_to = user_to + self.db_user_from = user_from + + self.has_errors = not is_valid + return is_valid + + def save(self, commit=True): + if self.has_errors: + raise ValueError( + "The %s could not be saved because the data didn't validate." + % (self.instance._meta.object_name) + ) + if not self.confirm.data: + self.create_phantom_account() + self.prepare_instance() + self.create_automatic_trade() + + if commit: + db.session.commit() + + return self.instance + + def prepare_instance(self): + Model = db.Model._decl_class_registry.data['Trade']() + self.instance = Model() + self.instance.user_from = self.db_user_from + self.instance.user_to = self.db_user_to + self.instance.lot_id = self._lot.id + self.instance.devices = self._lot.devices + self.instance.code = self.code.data + self.instance.confirm = self.confirm.data + self.instance.date = self.date.data + self.instance.name = self.name.data + self.instance.description = self.description.data + db.session.add(self.instance) + + def create_phantom_account(self): + """ + If exist both users not to do nothing + If exist from but not to: + search if exist in the DB + if exist use it + else create new one + The same if exist to but not from + + """ + user_from = self.user_from.data + user_to = self.user_to.data + code = self.code.data + + if user_from and user_to: + # both users exist, no further action is necessary + return + + # Create receiver (to) phantom account + if user_from and not user_to: + assert g.user.email == user_from + + self.user_from = g.user + self.user_to = self.get_or_create_user(code) + return + + # Create supplier (from) phantom account + if not user_from and user_to: + assert g.user.email == user_to + + self.user_from = self.get_or_create_user(code) + self.user_to = g.user + + def get_or_create_user(self, code): + email = "{}_{}@dhub.com".format(str(g.user.id), code) + user = User.query.filter_by(email=email).first() + if not user: + user = User(email=email, password='', active=False, phantom=True) + db.session.add(user) + return user + + def create_automatic_trade(self): + # This method change the ownership of devices + # do nothing if an explicit confirmation is required + if self.confirm.data: + return + + # Change the owner for every devices + for dev in self._lot.devices: + dev.change_owner(self.db_user_to) + + def check_valid(self): + if self.user_from.data == self.user_to.data: + return + + if self.user_from.data == g.user.email: + return 'user_to' + + if self.user_to.data == g.user.email: + return 'user_from' + + +class TradeDocumentForm(FlaskForm): + url = URLField( + 'Url', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Url where the document resides", + ) + description = StringField( + 'Description', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="", + ) + id_document = StringField( + 'Document Id', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="Identification number of document", + ) + date = DateField( + 'Date', + [validators.Optional()], + render_kw={'class': "form-control"}, + description="", + ) + file_name = FileField( + 'File', + [validators.DataRequired()], + render_kw={'class': "form-control"}, + description="""This file is not stored on our servers, it is only used to + generate a digital signature and obtain the name of the file.""", + ) + + def __init__(self, *args, **kwargs): + lot_id = kwargs.pop('lot') + super().__init__(*args, **kwargs) + self._lot = Lot.query.filter(Lot.id == lot_id).one() + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if g.user not in [self._lot.trade.user_from, self._lot.trade.user_to]: + is_valid = False + + return is_valid + + def save(self, commit=True): + file_name = '' + file_hash = '' + if self.file_name.data: + file_name = self.file_name.data.filename + file_hash = insert_hash(self.file_name.data.read(), commit=False) + + self.url.data = URL(self.url.data) + self._obj = TradeDocument(lot_id=self._lot.id) + self.populate_obj(self._obj) + self._obj.file_name = file_name + self._obj.file_hash = file_hash + db.session.add(self._obj) + self._lot.trade.documents.add(self._obj) + if commit: + db.session.commit() + + return self._obj diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 40764189..5df42ac3 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -1,7 +1,12 @@ +import csv +from io import StringIO + import flask -from flask import Blueprint, request, url_for +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 werkzeug.exceptions import NotFound from ereuse_devicehub import messages from ereuse_devicehub.inventory.forms import ( @@ -14,9 +19,14 @@ from ereuse_devicehub.inventory.forms import ( TagDeviceForm, TagForm, TagUnnamedForm, + TradeDocumentForm, + TradeForm, UploadSnapshotForm, ) -from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.action.models import Trade +from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device +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 @@ -36,7 +46,7 @@ class DeviceListMix(View): lot = None tags = ( Tag.query.filter(Tag.owner_id == current_user.id) - .filter(Tag.device_id == None) + .filter(Tag.device_id.is_(None)) .order_by(Tag.created.desc()) ) @@ -47,17 +57,22 @@ class DeviceListMix(View): form_new_action = NewActionForm(lot=lot.id) form_new_allocate = AllocateForm(lot=lot.id) form_new_datawipe = DataWipeForm(lot=lot.id) + form_new_trade = TradeForm( + lot=lot.id, + user_to=g.user.email, + user_from=g.user.email, + ) else: devices = ( Device.query.filter(Device.owner_id == current_user.id) .filter(Device.type.in_(filter_types)) - .filter(Device.lots == None) + .filter_by(lots=None) .order_by(Device.updated.desc()) ) form_new_action = NewActionForm() form_new_allocate = AllocateForm() form_new_datawipe = DataWipeForm() - + form_new_trade = '' action_devices = form_new_action.devices.data list_devices = [] if action_devices: @@ -71,6 +86,7 @@ class DeviceListMix(View): 'form_new_action': form_new_action, 'form_new_allocate': form_new_allocate, 'form_new_datawipe': form_new_datawipe, + 'form_new_trade': form_new_trade, 'lot': lot, 'tags': tags, 'list_devices': list_devices, @@ -325,8 +341,10 @@ class NewActionView(View): self.form = self.form_class() if self.form.validate_on_submit(): - instance = self.form.save() - messages.success('Action "{}" created successfully!'.format(instance.type)) + self.form.save() + messages.success( + 'Action "{}" created successfully!'.format(self.form.type.data) + ) next_url = self.get_next_url() return flask.redirect(next_url) @@ -348,8 +366,10 @@ class NewAllocateView(NewActionView, DeviceListMix): self.form = self.form_class() if self.form.validate_on_submit(): - instance = self.form.save() - messages.success('Action "{}" created successfully!'.format(instance.type)) + self.form.save() + messages.success( + 'Action "{}" created successfully!'.format(self.form.type.data) + ) next_url = self.get_next_url() return flask.redirect(next_url) @@ -368,8 +388,10 @@ class NewDataWipeView(NewActionView, DeviceListMix): self.form = self.form_class() if self.form.validate_on_submit(): - instance = self.form.save() - messages.success('Action "{}" created successfully!'.format(instance.type)) + self.form.save() + messages.success( + 'Action "{}" created successfully!'.format(self.form.type.data) + ) next_url = self.get_next_url() return flask.redirect(next_url) @@ -380,13 +402,186 @@ class NewDataWipeView(NewActionView, DeviceListMix): return flask.render_template(self.template_name, **self.context) +class NewTradeView(NewActionView, DeviceListMix): + methods = ['POST'] + form_class = TradeForm + + def dispatch_request(self): + self.form = self.form_class() + + 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) + + 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) + + +class NewTradeDocumentView(View): + methods = ['POST', 'GET'] + decorators = [login_required] + template_name = 'inventory/trade_document.html' + form_class = TradeDocumentForm + title = "Add new document" + + def dispatch_request(self, lot_id): + self.form = self.form_class(lot=lot_id) + + if self.form.validate_on_submit(): + self.form.save() + messages.success('Document created successfully!') + next_url = url_for('inventory.devices.lotdevicelist', lot_id=lot_id) + return flask.redirect(next_url) + + return flask.render_template( + self.template_name, form=self.form, title=self.title + ) + + +class ExportsView(View): + methods = ['GET'] + decorators = [login_required] + + def dispatch_request(self, export_id): + export_ids = { + 'metrics': self.metrics, + 'devices': self.devices_list, + 'certificates': self.erasure, + 'links': self.public_links, + } + + if export_id not in export_ids: + return NotFound() + return export_ids[export_id]() + + def find_devices(self): + args = request.args.get('ids') + ids = args.split(',') if args else [] + query = Device.query.filter(Device.owner == g.user) + return query.filter(Device.devicehub_id.in_(ids)) + + def response_csv(self, data, name): + bfile = data.getvalue().encode('utf-8') + # insert proof + insert_hash(bfile) + output = make_response(bfile) + output.headers['Content-Disposition'] = 'attachment; filename={}'.format(name) + output.headers['Content-type'] = 'text/csv' + return output + + def devices_list(self): + """Get device query and put information in csv format.""" + data = StringIO() + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') + first = True + + for device in self.find_devices(): + d = DeviceRow(device, {}) + if first: + cw.writerow(d.keys()) + first = False + cw.writerow(d.values()) + + return self.response_csv(data, "export.csv") + + def metrics(self): + """Get device query and put information in csv format.""" + data = StringIO() + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') + first = True + devs_id = [] + # Get the allocate info + for device in self.find_devices(): + devs_id.append(device.id) + for allocate in device.get_metrics(): + d = ActionRow(allocate) + if first: + cw.writerow(d.keys()) + first = False + cw.writerow(d.values()) + + # Get the trade info + query_trade = Trade.query.filter( + Trade.devices.any(Device.id.in_(devs_id)) + ).all() + + lot_id = request.args.get('lot') + if lot_id and not query_trade: + lot = Lot.query.filter_by(id=lot_id).one() + if hasattr(lot, "trade") and lot.trade: + if g.user in [lot.trade.user_from, lot.trade.user_to]: + query_trade = [lot.trade] + + for trade in query_trade: + data_rows = trade.get_metrics() + for row in data_rows: + d = ActionRow(row) + if first: + cw.writerow(d.keys()) + first = False + cw.writerow(d.values()) + + 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( + flask_weasyprint.HTML(string=template), + download_filename='erasure-certificate.pdf', + ) + insert_hash(res.data) + return res + + def build_erasure_certificate(self): + erasures = [] + for device in self.find_devices(): + if isinstance(device, Computer): + for privacy in device.privacy: + erasures.append(privacy) + elif isinstance(device, DataStorage): + if device.privacy: + erasures.append(device.privacy) + + params = { + 'title': 'Erasure Certificate', + 'erasures': tuple(erasures), + 'url_pdf': '', + } + return flask.render_template('inventory/erasure.html', **params) + + devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add')) +devices.add_url_rule('/action/trade/add/', view_func=NewTradeView.as_view('trade_add')) devices.add_url_rule( '/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add') ) devices.add_url_rule( '/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add') ) +devices.add_url_rule( + '/lot//trade-document/add/', + view_func=NewTradeDocumentView.as_view('trade_document_add'), +) devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) devices.add_url_rule( '/device//', view_func=DeviceDetailView.as_view('device_details') @@ -424,3 +619,6 @@ devices.add_url_rule( '/tag/devices//del/', view_func=TagUnlinkDeviceView.as_view('tag_devices_del'), ) +devices.add_url_rule( + '/export//', view_func=ExportsView.as_view('export') +) diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index 1eb680fd..d790af81 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -1,12 +1,16 @@ $(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'); if (show_allocate_form != "None") { $("#allocateModal .btn-primary").show(); newAllocate(show_allocate_form); } else if (show_datawipe_form != "None") { $("#datawipeModal .btn-primary").show(); newDataWipe(show_datawipe_form); + } else if (show_trade_form != "None") { + $("#tradeLotModal .btn-primary").show(); + newTrade(show_trade_form); } else { $(".deviceSelect").on("change", deviceSelect); } @@ -15,6 +19,7 @@ $(document).ready(function() { function deviceSelect() { var devices_count = $(".deviceSelect").filter(':checked').length; + get_device_list(); if (devices_count == 0) { $("#addingLotModal .pol").show(); $("#addingLotModal .btn-primary").hide(); @@ -50,23 +55,48 @@ function deviceSelect() { $("#datawipeModal .btn-primary").show(); $("#addingTagModal .pol").hide(); + $("#addingTagModal .btn-primary").show(); } } function removeTag() { var devices = $(".deviceSelect").filter(':checked'); var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); - console.log(devices_id); if (devices_id.length > 0) { var url = "/inventory/tag/devices/"+devices_id[0]+"/del/"; window.location.href = url; } } +function addTag() { + deviceSelect(); + $("#addingTagModal").click(); +} + +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(''); + $("#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(''); + $("#user_from").val(user_from); + } + $("#tradeLotModal #title-action").html(title); + $("#activeTradeModal").click(); +} + function newAction(action) { $("#actionModal #type").val(action); $("#actionModal #title-action").html(action); - get_device_list(); deviceSelect(); $("#activeActionModal").click(); } @@ -74,7 +104,6 @@ function newAction(action) { function newAllocate(action) { $("#allocateModal #type").val(action); $("#allocateModal #title-action").html(action); - get_device_list(); deviceSelect(); $("#activeAllocateModal").click(); } @@ -82,7 +111,6 @@ function newAllocate(action) { function newDataWipe(action) { $("#datawipeModal #type").val(action); $("#datawipeModal #title-action").html(action); - get_device_list(); deviceSelect(); $("#activeDatawipeModal").click(); } @@ -120,3 +148,14 @@ function get_device_list() { description = $.map(list_devices, function(x) { return 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; + window.location.href = url; + } else { + $("#exportAlertModal").click(); + } +} diff --git a/ereuse_devicehub/templates/inventory/addDevicestag.html b/ereuse_devicehub/templates/inventory/addDevicestag.html index 9c157729..cb929d37 100644 --- a/ereuse_devicehub/templates/inventory/addDevicestag.html +++ b/ereuse_devicehub/templates/inventory/addDevicestag.html @@ -24,7 +24,7 @@ diff --git a/ereuse_devicehub/templates/inventory/alert_export_error.html b/ereuse_devicehub/templates/inventory/alert_export_error.html new file mode 100644 index 00000000..8212a364 --- /dev/null +++ b/ereuse_devicehub/templates/inventory/alert_export_error.html @@ -0,0 +1,21 @@ + diff --git a/ereuse_devicehub/templates/inventory/allocate.html b/ereuse_devicehub/templates/inventory/allocate.html index fb16ce86..989bc74b 100644 --- a/ereuse_devicehub/templates/inventory/allocate.html +++ b/ereuse_devicehub/templates/inventory/allocate.html @@ -1,5 +1,5 @@