diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index c94342ed..0d77ad48 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -1,11 +1,23 @@ +import json from flask_wtf import FlaskForm -from wtforms import StringField, validators -from flask import g +from wtforms import StringField, validators, MultipleFileField, FloatField, IntegerField +from flask import g, request +from sqlalchemy.util import OrderedSet +from json.decoder import JSONDecodeError from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.device.models import Device, Computer, Smartphone, Cellphone, \ + Tablet, Monitor, Mouse, Keyboard, \ + MemoryCardReader, SAI +from ereuse_devicehub.resources.action.models import RateComputer, Snapshot, VisualTest +from ereuse_devicehub.resources.action.schemas import Snapshot as SnapshotSchema from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.tag.model import Tag +from ereuse_devicehub.resources.enums import SnapshotSoftware, Severity +from ereuse_devicehub.resources.user.exceptions import InsufficientPermission +from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate +from ereuse_devicehub.resources.device.sync import Sync +from ereuse_devicehub.resources.action.views.snapshot import save_json, move_json class LotDeviceForm(FlaskForm): @@ -55,18 +67,16 @@ class LotForm(FlaskForm): self.name.data = self.instance.name def save(self): - name = self.name.data.strip() - if self.instance: - if self.instance.name == name: - return self.instance - self.instance.name = name - else: - self.instance = Lot(name=name) + if not self.id: + self.instance = Lot(name=self.name.data) + + self.populate_obj(self.instance) if not self.id: + self.id = self.instance.id db.session.add(self.instance) db.session.commit() - return self.instance + return self.id db.session.commit() return self.id @@ -78,6 +88,290 @@ class LotForm(FlaskForm): return self.instance +class UploadSnapshotForm(FlaskForm): + snapshot = MultipleFileField(u'Select a Snapshot File', [validators.DataRequired()]) + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + data = request.files.getlist(self.snapshot.name) + if not data: + return False + self.snapshots = [] + self.result = {} + for d in data: + filename = d.filename + self.result[filename] = 'Not processed' + d = d.stream.read() + if not d: + self.result[filename] = 'Error this snapshot is empty' + continue + + try: + d_json = json.loads(d) + except JSONDecodeError: + self.result[filename] = 'Error this snapshot is not a json' + continue + + uuid_snapshot = d_json.get('uuid') + if Snapshot.query.filter(Snapshot.uuid == uuid_snapshot).all(): + self.result[filename] = 'Error this snapshot exist' + continue + + self.snapshots.append((filename, d_json)) + + if not self.snapshots: + return False + + return True + + def save(self): + if any([x == 'Error' for x in self.result.values()]): + return + # result = [] + self.sync = Sync() + schema = SnapshotSchema() + # self.tmp_snapshots = app.config['TMP_SNAPSHOTS'] + # TODO @cayop get correct var config + self.tmp_snapshots = '/tmp/' + for filename, snapshot_json in self.snapshots: + path_snapshot = save_json(snapshot_json, self.tmp_snapshots, g.user.email) + snapshot_json.pop('debug', None) + snapshot_json = schema.load(snapshot_json) + response = self.build(snapshot_json) + + if hasattr(response, 'type'): + self.result[filename] = 'Ok' + else: + self.result[filename] = 'Error' + + move_json(self.tmp_snapshots, path_snapshot, g.user.email) + + db.session.commit() + return response + + def build(self, snapshot_json): + # 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 isinstance(device, Computer) and device.hid: + device.add_mac_to_hid(components_snap=components) + snapshot = Snapshot(**snapshot_json) + + # Remove new actions from devices so they don't interfere with sync + 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) + for component in components: + component.actions_one.clear() + + assert not device.actions_one + assert all(not c.actions_one for c in components) if components else True + db_device, remove_actions = self.sync.run(device, components) + + del device # Do not use device anymore + snapshot.device = db_device + snapshot.actions |= remove_actions | actions_device # Set actions to snapshot + # commit will change the order of the components by what + # the DB wants. Let's get a copy of the list so we preserve order + ordered_components = OrderedSet(x for x in snapshot.components) + + # Add the new actions to the db-existing devices and components + db_device.actions_one |= actions_device + if components: + for component, actions in zip(ordered_components, actions_components): + component.actions_one |= actions + snapshot.actions |= actions + + if snapshot.software == SnapshotSoftware.Workbench: + # Check ownership of (non-component) device to from current.user + if db_device.owner_id != g.user.id: + raise InsufficientPermission() + # Compute ratings + try: + rate_computer, price = RateComputer.compute(db_device) + except CannotRate: + pass + else: + snapshot.actions.add(rate_computer) + if price: + snapshot.actions.add(price) + elif snapshot.software == SnapshotSoftware.WorkbenchAndroid: + pass # TODO try except to compute RateMobile + # Check if HID is null and add Severity:Warning to Snapshot + if snapshot.device.hid is None: + snapshot.severity = Severity.Warning + + db.session.add(snapshot) + return snapshot + + +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()]) + + 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} + + if not self.generation.data: + self.generation.data = 1 + + if not self.weight.data: + self.weight.data = 0.1 + + if not self.height.data: + self.height.data = 0.1 + + if not self.width.data: + self.width.data = 0.1 + + if not self.depth.data: + self.depth.data = 0.1 + + def validate(self, extra_validators=None): + error = ["Not a correct value"] + is_valid = super().validate(extra_validators) + + if self.generation.data < 1: + self.generation.errors = error + is_valid = False + + if self.weight.data < 0.1: + self.weight.errors = error + is_valid = False + + if self.height.data < 0.1: + self.height.errors = error + is_valid = False + + if self.width.data < 0.1: + self.width.errors = error + is_valid = False + + if self.depth.data < 0.1: + self.depth.errors = error + is_valid = False + + if self.imei.data: + if not 13 < len(str(self.imei.data)) < 17: + self.imei.errors = error + is_valid = False + + if self.meid.data: + meid = self.meid.data + if not 13 < len(meid) < 17: + is_valid = False + try: + int(meid, 16) + except ValueError: + self.meid.errors = error + is_valid = False + + if not is_valid: + return False + + if self.image.data == '': + self.image.data = None + if self.manufacturer.data: + self.manufacturer.data = self.manufacturer.data.lower() + if self.model.data: + self.model.data = self.model.data.lower() + if self.serial_number.data: + self.serial_number.data = self.serial_number.data.lower() + + return True + + def save(self): + + json_snapshot = { + 'type': 'Snapshot', + '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 + } + } + + if self.appearance.data or self.functionality.data: + json_snapshot['device']['actions'] = [{ + 'type': 'VisualTest', + 'appearanceRange': self.appearance.data, + 'functionalityRange': self.functionality.data + }] + + upload_form = UploadSnapshotForm() + upload_form.sync = Sync() + + schema = SnapshotSchema() + self.tmp_snapshots = '/tmp/' + path_snapshot = save_json(json_snapshot, self.tmp_snapshots, g.user.email) + snapshot_json = schema.load(json_snapshot) + + if self.type.data == 'Monitor': + snapshot_json['device'].resolution_width = self.resolution.data + snapshot_json['device'].size = self.screen.data + + if self.type.data in ['Smartphone', 'Tablet', 'Cellphone']: + snapshot_json['device'].imei = self.imei.data + snapshot_json['device'].meid = self.meid.data + + snapshot = upload_form.build(snapshot_json) + + move_json(self.tmp_snapshots, path_snapshot, g.user.email) + if self.type.data == 'Monitor': + snapshot.device.resolution = self.resolution.data + snapshot.device.screen = self.screen.data + + db.session.commit() + return snapshot + + class TagForm(FlaskForm): code = StringField(u'Code', [validators.length(min=1)]) @@ -99,6 +393,3 @@ class NewActionForm(FlaskForm): date = StringField(u'Date') severity = StringField(u'Severity') description = StringField(u'Description') - - def save(self): - pass diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 60ca57e8..7de67fae 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -1,12 +1,13 @@ import flask from flask.views import View -from flask import Blueprint, url_for +from flask import Blueprint, url_for, request from flask_login import login_required, current_user from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.device.models import Device -from ereuse_devicehub.inventory.forms import LotDeviceForm, LotForm, TagForm +from ereuse_devicehub.inventory.forms import LotDeviceForm, LotForm, UploadSnapshotForm, \ + NewDeviceForm, TagForm devices = Blueprint('inventory.devices', __name__, url_prefix='/inventory') @@ -15,18 +16,21 @@ class DeviceListView(View): decorators = [login_required] template_name = 'inventory/device_list.html' - def dispatch_request(self, id=None): + def dispatch_request(self, lot_id=None): # TODO @cayop adding filter + # https://github.com/eReuse/devicehub-teal/blob/testing/ereuse_devicehub/resources/device/views.py#L56 filter_types = ['Desktop', 'Laptop', 'Server'] lots = Lot.query.filter(Lot.owner_id == current_user.id) lot = None - if id: - lot = lots.filter(Lot.id == id).one() + if lot_id: + lot = lots.filter(Lot.id == lot_id).one() devices = [dev for dev in lot.devices if dev.type in filter_types] + devices = sorted(devices, key=lambda x: x.updated, reverse=True) else: devices = Device.query.filter( Device.owner_id == current_user.id).filter( - Device.type.in_(filter_types)).filter(Device.lots == None) + Device.type.in_(filter_types)).filter(Device.lots == None).order_by( + Device.updated.desc()) context = {'devices': devices, 'lots': lots, @@ -35,6 +39,20 @@ class DeviceListView(View): return flask.render_template(self.template_name, **context) +class DeviceDetailsView(View): + decorators = [login_required] + template_name = 'inventory/device_details.html' + + def dispatch_request(self, id): + lots = Lot.query.filter(Lot.owner_id == current_user.id) + device = Device.query.filter( + Device.owner_id == current_user.id).filter(Device.devicehub_id == id).one() + + context = {'device': device, + 'lots': lots} + return flask.render_template(self.template_name, **context) + + class LotDeviceAddView(View): methods = ['POST'] decorators = [login_required] @@ -45,8 +63,7 @@ class LotDeviceAddView(View): if form.validate_on_submit(): form.save() - next_url = url_for('inventory.devices.lotdevicelist', id=form.lot.data) - return flask.redirect(next_url) + return flask.redirect(request.referrer) class LotDeviceDeleteView(View): @@ -59,29 +76,42 @@ class LotDeviceDeleteView(View): if form.validate_on_submit(): form.remove() - next_url = url_for('inventory.devices.lotdevicelist', id=form.lot.data) - return flask.redirect(next_url) + # TODO @cayop It's possible this redirect not work in production + return flask.redirect(request.referrer) -class LotView(View): +class LotCreateView(View): methods = ['GET', 'POST'] decorators = [login_required] template_name = 'inventory/lot.html' title = "Add a new lot" - def dispatch_request(self, id=None): - if id: - self.title = "Edit lot" + def dispatch_request(self): + form = LotForm() + if form.validate_on_submit(): + form.save() + next_url = url_for('inventory.devices.lotdevicelist', lot_id=form.id) + return flask.redirect(next_url) + + lots = Lot.query.filter(Lot.owner_id == current_user.id) + return flask.render_template(self.template_name, form=form, title=self.title, lots=lots) + + +class LotUpdateView(View): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/lot.html' + title = "Edit a new lot" + + def dispatch_request(self, id): form = LotForm(id=id) if form.validate_on_submit(): form.save() - lot_id = id - if not id: - lot_id = form.instance.id - next_url = url_for('inventory.devices.lotdevicelist', id=lot_id) + next_url = url_for('inventory.devices.lotdevicelist', lot_id=id) return flask.redirect(next_url) - return flask.render_template(self.template_name, form=form, title=self.title) + lots = Lot.query.filter(Lot.owner_id == current_user.id) + return flask.render_template(self.template_name, form=form, title=self.title, lots=lots) class LotDeleteView(View): @@ -96,6 +126,36 @@ class LotDeleteView(View): return flask.redirect(next_url) +class UploadSnapshotView(View): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/upload_snapshot.html' + + def dispatch_request(self): + lots = Lot.query.filter(Lot.owner_id == current_user.id).all() + form = UploadSnapshotForm() + if form.validate_on_submit(): + form.save() + + return flask.render_template(self.template_name, form=form, lots=lots) + + +class CreateDeviceView(View): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/create_device.html' + + def dispatch_request(self): + lots = Lot.query.filter(Lot.owner_id == current_user.id).all() + form = NewDeviceForm() + if form.validate_on_submit(): + form.save() + next_url = url_for('inventory.devices.devicelist') + return flask.redirect(next_url) + + return flask.render_template(self.template_name, form=form, lots=lots) + + class TagListView(View): methods = ['GET'] decorators = [login_required] @@ -125,11 +185,14 @@ class TagAddView(View): devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) -devices.add_url_rule('/lot//device/', view_func=DeviceListView.as_view('lotdevicelist')) +devices.add_url_rule('/device//', view_func=DeviceDetailsView.as_view('device_details')) +devices.add_url_rule('/lot//device/', view_func=DeviceListView.as_view('lotdevicelist')) devices.add_url_rule('/lot/devices/add/', view_func=LotDeviceAddView.as_view('lot_devices_add')) devices.add_url_rule('/lot/devices/del/', view_func=LotDeviceDeleteView.as_view('lot_devices_del')) -devices.add_url_rule('/lot/add/', view_func=LotView.as_view('lot_add')) +devices.add_url_rule('/lot/add/', view_func=LotCreateView.as_view('lot_add')) devices.add_url_rule('/lot//del/', view_func=LotDeleteView.as_view('lot_del')) -devices.add_url_rule('/lot//', view_func=LotView.as_view('lot_edit')) +devices.add_url_rule('/lot//', view_func=LotUpdateView.as_view('lot_edit')) +devices.add_url_rule('/upload-snapshot/', view_func=UploadSnapshotView.as_view('upload_snapshot')) +devices.add_url_rule('/device/add/', view_func=CreateDeviceView.as_view('device_add')) devices.add_url_rule('/tag/', view_func=TagListView.as_view('taglist')) devices.add_url_rule('/tag/add/', view_func=TagAddView.as_view('tag_add')) diff --git a/ereuse_devicehub/static/js/create_device.js b/ereuse_devicehub/static/js/create_device.js new file mode 100644 index 00000000..1c9e0655 --- /dev/null +++ b/ereuse_devicehub/static/js/create_device.js @@ -0,0 +1,23 @@ +$(document).ready(function() { + $("#type").on("change", deviceInputs); + deviceInputs(); +}) + +function deviceInputs() { + if ($("#type").val() == 'Monitor') { + $("#screen").show(); + $("#resolution").show(); + $("#imei").hide(); + $("#meid").hide(); + } else if (['Smartphone', 'Cellphone', 'Tablet'].includes($("#type").val())) { + $("#screen").hide(); + $("#resolution").hide(); + $("#imei").show(); + $("#meid").show(); + } else { + $("#screen").hide(); + $("#resolution").hide(); + $("#imei").hide(); + $("#meid").hide(); + } +} diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index e4ae17f6..94f78723 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -105,7 +105,7 @@ {% for lot in lots %} {% if lot.is_incominig %}
  • - + {{ lot.name }}
  • @@ -122,7 +122,7 @@ {% for lot in lots %} {% if lot.is_outgoing %}
  • - + {{ lot.name }}
  • @@ -133,18 +133,18 @@