diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index ca2338ea..e9430e63 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -1,24 +1,31 @@ import json -from flask_wtf import FlaskForm -from wtforms import StringField, validators, MultipleFileField, FloatField, IntegerField, \ - SelectField -from flask import g, request -from sqlalchemy.util import OrderedSet from json.decoder import JSONDecodeError +from flask import g, request +from flask_wtf import FlaskForm +from sqlalchemy.util import OrderedSet +from wtforms import (DateField, FloatField, HiddenField, IntegerField, + MultipleFileField, SelectField, StringField, + TextAreaField, validators) + from ereuse_devicehub.db import db -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.action.models import (Action, RateComputer, + Snapshot, VisualTest) +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 ereuse_devicehub.resources.device.sync import Sync +from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware 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): @@ -418,7 +425,6 @@ class TagDeviceForm(FlaskForm): self.delete = kwargs.pop('delete', None) self.device_id = kwargs.pop('device', None) - # import pdb; pdb.set_trace() super().__init__(*args, **kwargs) if self.delete: @@ -466,7 +472,68 @@ class TagDeviceForm(FlaskForm): class NewActionForm(FlaskForm): - name = StringField(u'Name') - date = StringField(u'Date') - severity = StringField(u'Severity') - description = StringField(u'Description') + name = StringField(u'Name', [validators.length(max=50)]) + devices = HiddenField() + date = DateField(u'Date', validators=(validators.Optional(),)) + severity = SelectField(u'Severity', choices=[(v.name, v.name) for v in Severity]) + description = TextAreaField(u'Description') + lot = HiddenField() + type = HiddenField() + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + return False + + 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 not self._devices: + return False + + return True + + def save(self): + Model = db.Model._decl_class_registry.data[self.type.data]() + self.instance = Model() + devices = self.devices.data + severity = self.severity.data + self.devices.data = self._devices + self.severity.data = Severity[self.severity.data] + + self.populate_obj(self.instance) + db.session.add(self.instance) + db.session.commit() + + self.devices.data = devices + self.severity.data = severity + + return self.instance + + +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') + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + start_time = self.start_time.data + end_time = self.end_time.data + if start_time and end_time and end_time < start_time: + error = ['The action cannot finish before it starts.'] + self.start_time.errors = error + self.end_time.errors = error + is_valid = False + + if not self.end_users.data: + self.end_users.errors = ["You need to specify a number of users"] + is_valid = False + + return is_valid diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 54ede74a..bb809ec7 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -1,23 +1,27 @@ import flask +from flask import Blueprint, request, url_for from flask.views import View -from flask import Blueprint, url_for, request -from flask_login import login_required, current_user +from flask_login import current_user, login_required +from ereuse_devicehub import messages +from ereuse_devicehub.inventory.forms import (AllocateForm, LotDeviceForm, + LotForm, NewActionForm, + NewDeviceForm, TagDeviceForm, + TagForm, TagUnnamedForm, + UploadSnapshotForm) +from ereuse_devicehub.resources.device.models import Device 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, UploadSnapshotForm, \ - NewDeviceForm, TagForm, TagUnnamedForm, TagDeviceForm # TODO(@slamora): rename base 'inventory.devices' --> 'inventory' devices = Blueprint('inventory.devices', __name__, url_prefix='/inventory') -class DeviceListView(View): +class DeviceListMix(View): decorators = [login_required] template_name = 'inventory/device_list.html' - def dispatch_request(self, lot_id=None): + def get_context(self, lot_id): # TODO @cayop adding filter # https://github.com/eReuse/devicehub-teal/blob/testing/ereuse_devicehub/resources/device/views.py#L56 filter_types = ['Desktop', 'Laptop', 'Server'] @@ -30,19 +34,41 @@ class DeviceListView(View): 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) + form_new_action = NewActionForm(lot=lot.id) + form_new_allocate = AllocateForm(lot=lot.id) else: devices = Device.query.filter( Device.owner_id == current_user.id).filter( Device.type.in_(filter_types)).filter(Device.lots == None).order_by( Device.updated.desc()) + form_new_action = NewActionForm() + form_new_allocate = AllocateForm() - context = {'devices': devices, - 'lots': lots, - 'form_lot_device': LotDeviceForm(), - 'form_tag_device': TagDeviceForm(), - 'lot': lot, - 'tags': tags} - return flask.render_template(self.template_name, **context) + action_devices = form_new_action.devices.data + list_devices = [] + if action_devices: + list_devices.extend([int(x) for x in action_devices.split(",")]) + + self.context = { + 'devices': devices, + 'lots': lots, + 'form_lot_device': LotDeviceForm(), + 'form_tag_device': TagDeviceForm(), + 'form_new_action': form_new_action, + 'form_new_allocate': form_new_allocate, + 'lot': lot, + 'tags': tags, + 'list_devices': list_devices + } + + return self.context + + +class DeviceListView(DeviceListMix): + + def dispatch_request(self, lot_id=None): + self.get_context(lot_id) + return flask.render_template(self.template_name, **self.context) class DeviceDetailView(View): @@ -261,6 +287,52 @@ class TagUnlinkDeviceView(View): return flask.render_template(self.template_name, form=form, referrer=request.referrer) +class NewActionView(View): + methods = ['POST'] + decorators = [login_required] + form_class = NewActionForm + + def dispatch_request(self): + self.form = self.form_class() + + if self.form.validate_on_submit(): + instance = self.form.save() + messages.success('Action "{}" created successfully!'.format(instance.type)) + + next_url = self.get_next_url() + return flask.redirect(next_url) + + def get_next_url(self): + lot_id = self.form.lot.data + + if lot_id: + return url_for('inventory.devices.lotdevicelist', lot_id=lot_id) + + return url_for('inventory.devices.devicelist') + + +class NewAllocateView(NewActionView, DeviceListMix): + methods = ['POST'] + form_class = AllocateForm + + def dispatch_request(self): + self.form = self.form_class() + + if self.form.validate_on_submit(): + instance = self.form.save() + messages.success('Action "{}" created successfully!'.format(instance.type)) + + 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) + + +devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add')) +devices.add_url_rule('/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add')) devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) devices.add_url_rule('/device//', view_func=DeviceDetailView.as_view('device_details')) devices.add_url_rule('/lot//device/', view_func=DeviceListView.as_view('lotdevicelist')) diff --git a/ereuse_devicehub/messages.py b/ereuse_devicehub/messages.py new file mode 100644 index 00000000..ba3bf089 --- /dev/null +++ b/ereuse_devicehub/messages.py @@ -0,0 +1,64 @@ +from flask import flash, session + +DEBUG = 10 +INFO = 20 +SUCCESS = 25 +WARNING = 30 +ERROR = 40 + +DEFAULT_LEVELS = { + 'DEBUG': DEBUG, + 'INFO': INFO, + 'SUCCESS': SUCCESS, + 'WARNING': WARNING, + 'ERROR': ERROR, +} + +DEFAULT_TAGS = { + DEBUG: 'light', + INFO: 'info', + SUCCESS: 'success', + WARNING: 'warning', + ERROR: 'danger', +} + +DEFAULT_ICONS = { + DEFAULT_TAGS[DEBUG]: 'tools', + DEFAULT_TAGS[INFO]: 'info-circle', + DEFAULT_TAGS[SUCCESS]: 'check-circle', + DEFAULT_TAGS[WARNING]: 'exclamation-triangle', + DEFAULT_TAGS[ERROR]: 'exclamation-octagon', +} + + +def add_message(level, message): + level_tag = DEFAULT_TAGS[level] + if '_message_icon' not in session: + session['_message_icon'] = DEFAULT_ICONS + + flash(message, level_tag) + + +def debug(message): + """Add a message with the ``DEBUG`` level.""" + add_message(DEBUG, message) + + +def info(message): + """Add a message with the ``INFO`` level.""" + add_message(INFO, message) + + +def success(message): + """Add a message with the ``SUCCESS`` level.""" + add_message(SUCCESS, message) + + +def warning(message): + """Add a message with the ``WARNING`` level.""" + add_message(WARNING, message) + + +def error(message): + """Add a message with the ``ERROR`` level.""" + add_message(ERROR, message) diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index a213ced5..107be562 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -213,18 +213,6 @@ class Action(Thing): args[INHERIT_COND] = cls.id == Action.id return args - @validates('end_time') - def validate_end_time(self, _, end_time: datetime): - if self.start_time and end_time <= self.start_time: - raise ValidationError('The action cannot finish before it starts.') - return end_time - - @validates('start_time') - def validate_start_time(self, _, start_time: datetime): - if self.end_time and start_time >= self.end_time: - raise ValidationError('The action cannot start after it finished.') - return start_time - @property def date_str(self): return '{:%c}'.format(self.end_time) diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index bca1d8db..df93b107 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -55,6 +55,10 @@ class Action(Thing): if 'start_time' in data and data['start_time'] < unix_time: data['start_time'] = unix_time + if data.get('end_time') and data.get('start_time'): + if data['start_time'] > data['end_time']: + raise ValidationError('The action cannot finish before it starts.') + class ActionWithOneDevice(Action): __doc__ = m.ActionWithOneDevice.__doc__ diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index a97c7392..16b9df5b 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -164,6 +164,10 @@ class Device(Thing): super().__init__(**kw) self.set_hid() + @property + def reverse_actions(self) -> list: + return reversed(self.actions) + @property def actions(self) -> list: """All the actions where the device participated, including: diff --git a/ereuse_devicehub/static/js/main_inventory.js b/ereuse_devicehub/static/js/main_inventory.js index ab499ee4..05833fe5 100644 --- a/ereuse_devicehub/static/js/main_inventory.js +++ b/ereuse_devicehub/static/js/main_inventory.js @@ -1,31 +1,46 @@ $(document).ready(function() { - $(".deviceSelect").on("change", deviceSelect); - // $('#selectLot').selectpicker(); + var show_action_form = $("#allocateModal").data('show-action-form'); + if (show_action_form != "None") { + $("#allocateModal .btn-primary").show(); + newAllocate(show_action_form); + } else { + $(".deviceSelect").on("change", deviceSelect); + } + // $('#selectLot').selectpicker(); }) function deviceSelect() { - var devices = $(".deviceSelect").filter(':checked'); - var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(","); - if (devices_id == "") { - $("#addingLotModal .text-danger").show(); + var devices_count = $(".deviceSelect").filter(':checked').length; + if (devices_count == 0) { + $("#addingLotModal .pol").show(); $("#addingLotModal .btn-primary").hide(); - $("#removeLotModal .text-danger").show(); + + $("#removeLotModal .pol").show(); $("#removeLotModal .btn-primary").hide(); - $("#addingTagModal .text-danger").show(); + + $("#addingTagModal .pol").show(); $("#addingTagModal .btn-primary").hide(); + + $("#actionModal .pol").show(); + $("#actionModal .btn-primary").hide(); + + $("#allocateModal .pol").show(); + $("#allocateModal .btn-primary").hide(); } else { - $("#addingLotModal .text-danger").hide(); - $("#addingLotModal .btn-primary").removeClass('d-none'); + $("#addingLotModal .pol").hide(); $("#addingLotModal .btn-primary").show(); - $("#removeLotModal .text-danger").hide(); - $("#removeLotModal .btn-primary").removeClass('d-none'); + + $("#removeLotModal .pol").hide(); $("#removeLotModal .btn-primary").show(); - $("#addingTagModal .text-danger").hide(); - $("#addingTagModal .btn-primary").removeClass('d-none'); + + $("#actionModal .pol").hide(); + $("#actionModal .btn-primary").show(); + + $("#allocateModal .pol").hide(); + $("#allocateModal .btn-primary").show(); + + $("#addingTagModal .pol").hide(); } - $.map($(".devicesList"), function(x) { - $(x).val(devices_id); - }); } function removeTag() { @@ -39,5 +54,50 @@ function removeTag() { } function newAction(action) { - console.log(action); + $("#actionModal #type").val(action); + $("#actionModal #title-action").html(action); + get_device_list(); + deviceSelect(); + $("#activeActionModal").click(); +} + +function newAllocate(action) { + $("#allocateModal #type").val(action); + $("#allocateModal #title-action").html(action); + get_device_list(); + deviceSelect(); + $("#activeAllocateModal").click(); +} + +function get_device_list() { + var devices = $(".deviceSelect").filter(':checked'); + + /* Insert the correct count of devices in actions form */ + var devices_count = devices.length; + $("#allocateModal .devices-count").html(devices_count); + $("#actionModal .devices-count").html(devices_count); + + /* Insert the correct value in the input devicesList */ + var devices_id = $.map(devices, function(x) { return $(x).attr('data')}).join(","); + $.map($(".devicesList"), function(x) { + $(x).val(devices_id); + }); + + /* Create a list of devices for human representation */ + var computer = { + "Desktop": "", + "Laptop": "", + }; + list_devices = devices.map(function (x) { + var typ = $(devices[x]).data("device-type"); + var manuf = $(devices[x]).data("device-manufacturer"); + var dhid = $(devices[x]).data("device-dhid"); + if (computer[typ]) { + typ = computer[typ]; + }; + return typ + " " + manuf + " " + dhid; + }); + + description = $.map(list_devices, function(x) { return x }).join(", "); + $(".enumeration-devices").html(description); } diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index 99cf0cf5..5bd1994c 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -175,6 +175,15 @@
+ {% block messages %} + {% for level, message in get_flashed_messages(with_categories=true) %} + + {% endfor %} + {% endblock %} {% block main %} {% endblock main %} diff --git a/ereuse_devicehub/templates/inventory/actions.html b/ereuse_devicehub/templates/inventory/actions.html index 2800c921..f3fc4a7f 100644 --- a/ereuse_devicehub/templates/inventory/actions.html +++ b/ereuse_devicehub/templates/inventory/actions.html @@ -1,25 +1,51 @@