Merge pull request #203 from eReuse/feature/server-side-render-actions-trade

Feature/server side render actions trade
This commit is contained in:
Santiago L 2022-02-28 10:18:33 +01:00 committed by GitHub
commit 7895cf457a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 887 additions and 300 deletions

View File

@ -3,35 +3,39 @@ import json
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from boltons.urlutils import URL 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.db import db
from ereuse_devicehub.resources.action.models import RateComputer, Snapshot from ereuse_devicehub.resources.action.models import RateComputer, Snapshot
from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate
from ereuse_devicehub.resources.action.schemas import \ from ereuse_devicehub.resources.action.schemas import \
Snapshot as SnapshotSchema Snapshot as SnapshotSchema
from ereuse_devicehub.resources.action.views.snapshot import move_json, save_json from ereuse_devicehub.resources.action.views.snapshot import (
from ereuse_devicehub.resources.device.models import (SAI, Cellphone, Computer, move_json, save_json)
Device, Keyboard, MemoryCardReader, from ereuse_devicehub.resources.device.models import (
Monitor, Mouse, Smartphone, Tablet) SAI, Cellphone, Computer, Device, Keyboard, MemoryCardReader, Monitor,
from flask import g, request Mouse, Smartphone, Tablet)
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.device.sync import Sync from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.documents.models import DataWipeDocument from ereuse_devicehub.resources.documents.models import DataWipeDocument
from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware from ereuse_devicehub.resources.enums import Severity, SnapshotSoftware
from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.hash_reports import insert_hash
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag 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.exceptions import InsufficientPermission
from ereuse_devicehub.resources.user.models import User
class LotDeviceForm(FlaskForm): class LotDeviceForm(FlaskForm):
lot = StringField(u'Lot', [validators.UUID()]) lot = StringField('Lot', [validators.UUID()])
devices = StringField(u'Devices', [validators.length(min=1)]) devices = StringField('Devices', [validators.length(min=1)])
def validate(self, extra_validators=None): def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators) is_valid = super().validate(extra_validators)
@ -39,16 +43,29 @@ class LotDeviceForm(FlaskForm):
if not is_valid: if not is_valid:
return False return False
self._lot = Lot.query.filter(Lot.id == self.lot.data).filter( self._lot = (
Lot.owner_id == g.user.id).one() Lot.query.filter(Lot.id == self.lot.data)
.filter(Lot.owner_id == g.user.id)
.one()
)
devices = set(self.devices.data.split(",")) devices = set(self.devices.data.split(","))
self._devices = Device.query.filter(Device.id.in_(devices)).filter( self._devices = (
Device.owner_id == g.user.id).distinct().all() Device.query.filter(Device.id.in_(devices))
.filter(Device.owner_id == g.user.id)
.distinct()
.all()
)
return bool(self._devices) return bool(self._devices)
def save(self): 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) self._lot.devices.update(self._devices)
db.session.add(self._lot) db.session.add(self._lot)
db.session.commit() db.session.commit()
@ -60,14 +77,17 @@ class LotDeviceForm(FlaskForm):
class LotForm(FlaskForm): class LotForm(FlaskForm):
name = StringField(u'Name', [validators.length(min=1)]) name = StringField('Name', [validators.length(min=1)])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.id = kwargs.pop('id', None) self.id = kwargs.pop('id', None)
self.instance = None self.instance = None
if self.id: if self.id:
self.instance = Lot.query.filter(Lot.id == self.id).filter( self.instance = (
Lot.owner_id == g.user.id).one() Lot.query.filter(Lot.id == self.id)
.filter(Lot.owner_id == g.user.id)
.one()
)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.instance and not self.name.data: if self.instance and not self.name.data:
self.name.data = self.instance.name self.name.data = self.instance.name
@ -95,7 +115,7 @@ class LotForm(FlaskForm):
class UploadSnapshotForm(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): def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators) is_valid = super().validate(extra_validators)
@ -163,8 +183,10 @@ class UploadSnapshotForm(FlaskForm):
# this is a copy adaptated from ereuse_devicehub.resources.action.views.snapshot # this is a copy adaptated from ereuse_devicehub.resources.action.views.snapshot
device = snapshot_json.pop('device') # type: Computer device = snapshot_json.pop('device') # type: Computer
components = None components = None
if snapshot_json['software'] == (SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid): if snapshot_json['software'] == (
components = snapshot_json.pop('components', None) # type: List[Component] SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid
):
components = snapshot_json.pop('components', None)
if isinstance(device, Computer) and device.hid: if isinstance(device, Computer) and device.hid:
device.add_mac_to_hid(components_snap=components) device.add_mac_to_hid(components_snap=components)
snapshot = Snapshot(**snapshot_json) snapshot = Snapshot(**snapshot_json)
@ -173,7 +195,9 @@ class UploadSnapshotForm(FlaskForm):
actions_device = set(e for e in device.actions_one) actions_device = set(e for e in device.actions_one)
device.actions_one.clear() device.actions_one.clear()
if components: 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: for component in components:
component.actions_one.clear() component.actions_one.clear()
@ -219,38 +243,40 @@ class UploadSnapshotForm(FlaskForm):
class NewDeviceForm(FlaskForm): class NewDeviceForm(FlaskForm):
type = StringField(u'Type', [validators.DataRequired()]) type = StringField('Type', [validators.DataRequired()])
label = StringField(u'Label') label = StringField('Label')
serial_number = StringField(u'Seria Number', [validators.DataRequired()]) serial_number = StringField('Seria Number', [validators.DataRequired()])
model = StringField(u'Model', [validators.DataRequired()]) model = StringField('Model', [validators.DataRequired()])
manufacturer = StringField(u'Manufacturer', [validators.DataRequired()]) manufacturer = StringField('Manufacturer', [validators.DataRequired()])
appearance = StringField(u'Appearance', [validators.Optional()]) appearance = StringField('Appearance', [validators.Optional()])
functionality = StringField(u'Functionality', [validators.Optional()]) functionality = StringField('Functionality', [validators.Optional()])
brand = StringField(u'Brand') brand = StringField('Brand')
generation = IntegerField(u'Generation') generation = IntegerField('Generation')
version = StringField(u'Version') version = StringField('Version')
weight = FloatField(u'Weight', [validators.DataRequired()]) weight = FloatField('Weight', [validators.DataRequired()])
width = FloatField(u'Width', [validators.DataRequired()]) width = FloatField('Width', [validators.DataRequired()])
height = FloatField(u'Height', [validators.DataRequired()]) height = FloatField('Height', [validators.DataRequired()])
depth = FloatField(u'Depth', [validators.DataRequired()]) depth = FloatField('Depth', [validators.DataRequired()])
variant = StringField(u'Variant', [validators.Optional()]) variant = StringField('Variant', [validators.Optional()])
sku = StringField(u'SKU', [validators.Optional()]) sku = StringField('SKU', [validators.Optional()])
image = StringField(u'Image', [validators.Optional(), validators.URL()]) image = StringField('Image', [validators.Optional(), validators.URL()])
imei = IntegerField(u'IMEI', [validators.Optional()]) imei = IntegerField('IMEI', [validators.Optional()])
meid = StringField(u'MEID', [validators.Optional()]) meid = StringField('MEID', [validators.Optional()])
resolution = IntegerField(u'Resolution width', [validators.Optional()]) resolution = IntegerField('Resolution width', [validators.Optional()])
screen = FloatField(u'Screen size', [validators.Optional()]) screen = FloatField('Screen size', [validators.Optional()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.devices = {"Smartphone": Smartphone, self.devices = {
"Tablet": Tablet, "Smartphone": Smartphone,
"Cellphone": Cellphone, "Tablet": Tablet,
"Monitor": Monitor, "Cellphone": Cellphone,
"Mouse": Mouse, "Monitor": Monitor,
"Keyboard": Keyboard, "Mouse": Mouse,
"SAI": SAI, "Keyboard": Keyboard,
"MemoryCardReader": MemoryCardReader} "SAI": SAI,
"MemoryCardReader": MemoryCardReader,
}
if not self.generation.data: if not self.generation.data:
self.generation.data = 1 self.generation.data = 1
@ -327,29 +353,31 @@ class NewDeviceForm(FlaskForm):
'software': 'Web', 'software': 'Web',
'version': '11.0', 'version': '11.0',
'device': { 'device': {
'type': self.type.data, 'type': self.type.data,
'model': self.model.data, 'model': self.model.data,
'manufacturer': self.manufacturer.data, 'manufacturer': self.manufacturer.data,
'serialNumber': self.serial_number.data, 'serialNumber': self.serial_number.data,
'brand': self.brand.data, 'brand': self.brand.data,
'version': self.version.data, 'version': self.version.data,
'generation': self.generation.data, 'generation': self.generation.data,
'sku': self.sku.data, 'sku': self.sku.data,
'weight': self.weight.data, 'weight': self.weight.data,
'width': self.width.data, 'width': self.width.data,
'height': self.height.data, 'height': self.height.data,
'depth': self.depth.data, 'depth': self.depth.data,
'variant': self.variant.data, 'variant': self.variant.data,
'image': self.image.data 'image': self.image.data,
} },
} }
if self.appearance.data or self.functionality.data: if self.appearance.data or self.functionality.data:
json_snapshot['device']['actions'] = [{ json_snapshot['device']['actions'] = [
'type': 'VisualTest', {
'appearanceRange': self.appearance.data, 'type': 'VisualTest',
'functionalityRange': self.functionality.data 'appearanceRange': self.appearance.data,
}] 'functionalityRange': self.functionality.data,
}
]
upload_form = UploadSnapshotForm() upload_form = UploadSnapshotForm()
upload_form.sync = Sync() upload_form.sync = Sync()
@ -379,14 +407,14 @@ class NewDeviceForm(FlaskForm):
class TagForm(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): def validate(self, extra_validators=None):
error = ["This value is being used"] error = ["This value is being used"]
is_valid = super().validate(extra_validators) is_valid = super().validate(extra_validators)
if not is_valid: if not is_valid:
return False return False
tag = Tag.query.filter(Tag.id==self.code.data).all() tag = Tag.query.filter(Tag.id == self.code.data).all()
if tag: if tag:
self.code.errors = error self.code.errors = error
return False return False
@ -407,7 +435,7 @@ class TagForm(FlaskForm):
class TagUnnamedForm(FlaskForm): class TagUnnamedForm(FlaskForm):
amount = IntegerField(u'amount') amount = IntegerField('amount')
def save(self): def save(self):
num = self.amount.data num = self.amount.data
@ -419,8 +447,8 @@ class TagUnnamedForm(FlaskForm):
class TagDeviceForm(FlaskForm): class TagDeviceForm(FlaskForm):
tag = SelectField(u'Tag', choices=[]) tag = SelectField('Tag', choices=[])
device = StringField(u'Device', [validators.Optional()]) device = StringField('Device', [validators.Optional()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.delete = kwargs.pop('delete', None) self.delete = kwargs.pop('delete', None)
@ -429,9 +457,11 @@ class TagDeviceForm(FlaskForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.delete: 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: 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] self.tag.choices = [(tag.id, tag.id) for tag in tags]
@ -441,8 +471,11 @@ class TagDeviceForm(FlaskForm):
if not is_valid: if not is_valid:
return False return False
self._tag = Tag.query.filter(Tag.id == self.tag.data).filter( self._tag = (
Tag.owner_id == g.user.id).one() 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: if not self.delete and self._tag.device_id:
self.tag.errors = [("This tag is actualy in use.")] self.tag.errors = [("This tag is actualy in use.")]
@ -456,8 +489,11 @@ class TagDeviceForm(FlaskForm):
if self.device_id or self.device.data: if self.device_id or self.device.data:
self.device_id = 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( self._device = (
Device.owner_id == g.user.id).one() Device.query.filter(Device.id == self.device_id)
.filter(Device.owner_id == g.user.id)
.one()
)
return True return True
@ -473,38 +509,55 @@ class TagDeviceForm(FlaskForm):
class NewActionForm(FlaskForm): class NewActionForm(FlaskForm):
name = StringField(u'Name', [validators.length(max=50)], name = StringField(
description="A name or title of the event. Something to look for.") 'Name',
[validators.length(max=50)],
description="A name or title of the event. Something to look for.",
)
devices = HiddenField() devices = HiddenField()
date = DateField(u'Date', [validators.Optional()], date = DateField(
description="""When the action ends. For some actions like booking 'Date',
[validators.Optional()],
description="""When the action ends. For some actions like booking
the time when it expires, for others like renting the the time when it expires, for others like renting the
time that the end rents. For specific actions, it is the time that the end rents. For specific actions, it is the
time in which they are carried out; differs from created time in which they are carried out; differs from created
in that created is where the system receives the action.""") 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. severity = SelectField(
For example, failed events are set to Error""") 'Severity',
description = TextAreaField(u'Description') 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() lot = HiddenField()
type = HiddenField() type = HiddenField()
def validate(self, extra_validators=None): 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: if not is_valid:
return False return False
self._devices = OrderedSet()
if self.devices.data: if self.devices.data:
devices = set(self.devices.data.split(",")) devices = set(self.devices.data.split(","))
self._devices = OrderedSet(Device.query.filter(Device.id.in_(devices)).filter( self._devices = OrderedSet(
Device.owner_id == g.user.id).all()) Device.query.filter(Device.id.in_(devices))
.filter(Device.owner_id == g.user.id)
.all()
)
if not self._devices: if not self._devices:
return False return False
return True 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): def save(self):
Model = db.Model._decl_class_registry.data[self.type.data]() Model = db.Model._decl_class_registry.data[self.type.data]()
self.instance = Model() self.instance = Model()
@ -522,13 +575,20 @@ class NewActionForm(FlaskForm):
return self.instance 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): class AllocateForm(NewActionForm):
start_time = DateField(u'Start time') start_time = DateField('Start time')
end_time = DateField(u'End time') end_time = DateField('End time')
final_user_code = StringField(u'Final user code', [validators.length(max=50)]) final_user_code = StringField('Final user code', [validators.length(max=50)])
transaction = StringField(u'Transaction', [validators.length(max=50)]) transaction = StringField('Transaction', [validators.length(max=50)])
end_users = IntegerField(u'End users') end_users = IntegerField('End users')
def validate(self, extra_validators=None): def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators) is_valid = super().validate(extra_validators)
@ -549,19 +609,31 @@ class AllocateForm(NewActionForm):
class DataWipeDocumentForm(Form): class DataWipeDocumentForm(Form):
date = DateField(u'Date', [validators.Optional()], date = DateField(
description="Date when was data wipe") 'Date', [validators.Optional()], description="Date when was data wipe"
url = URLField(u'Url', [validators.Optional()], )
description="Url where the document resides") url = URLField(
success = BooleanField(u'Success', [validators.Optional()], 'Url', [validators.Optional()], description="Url where the document resides"
description="The erase was success or not?") )
software = StringField(u'Software', [validators.Optional()], success = BooleanField(
description="Which software has you use for erase the disks") 'Success', [validators.Optional()], description="The erase was success or not?"
id_document = StringField(u'Document Id', [validators.Optional()], )
description="Identification number of document") software = StringField(
file_name = FileField(u'File', [validators.DataRequired()], 'Software',
description="""This file is not stored on our servers, it is only used to [validators.Optional()],
generate a digital signature and obtain the name of the file.""") 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): def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators) is_valid = super().validate(extra_validators)
@ -614,3 +686,230 @@ class DataWipeForm(NewActionForm):
self.document = document self.document = document
return self.instance 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

View File

@ -1,5 +1,5 @@
import flask import flask
from flask import Blueprint, request, url_for from flask import Blueprint, g, request, url_for
from flask.views import View from flask.views import View
from flask_login import current_user, login_required from flask_login import current_user, login_required
@ -14,6 +14,8 @@ from ereuse_devicehub.inventory.forms import (
TagDeviceForm, TagDeviceForm,
TagForm, TagForm,
TagUnnamedForm, TagUnnamedForm,
TradeDocumentForm,
TradeForm,
UploadSnapshotForm, UploadSnapshotForm,
) )
from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.device.models import Device
@ -47,6 +49,11 @@ class DeviceListMix(View):
form_new_action = NewActionForm(lot=lot.id) form_new_action = NewActionForm(lot=lot.id)
form_new_allocate = AllocateForm(lot=lot.id) form_new_allocate = AllocateForm(lot=lot.id)
form_new_datawipe = DataWipeForm(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: else:
devices = ( devices = (
Device.query.filter(Device.owner_id == current_user.id) Device.query.filter(Device.owner_id == current_user.id)
@ -57,7 +64,7 @@ class DeviceListMix(View):
form_new_action = NewActionForm() form_new_action = NewActionForm()
form_new_allocate = AllocateForm() form_new_allocate = AllocateForm()
form_new_datawipe = DataWipeForm() form_new_datawipe = DataWipeForm()
form_new_trade = ''
action_devices = form_new_action.devices.data action_devices = form_new_action.devices.data
list_devices = [] list_devices = []
if action_devices: if action_devices:
@ -71,6 +78,7 @@ class DeviceListMix(View):
'form_new_action': form_new_action, 'form_new_action': form_new_action,
'form_new_allocate': form_new_allocate, 'form_new_allocate': form_new_allocate,
'form_new_datawipe': form_new_datawipe, 'form_new_datawipe': form_new_datawipe,
'form_new_trade': form_new_trade,
'lot': lot, 'lot': lot,
'tags': tags, 'tags': tags,
'list_devices': list_devices, 'list_devices': list_devices,
@ -323,7 +331,9 @@ class NewActionView(View):
if self.form.validate_on_submit(): if self.form.validate_on_submit():
instance = self.form.save() instance = self.form.save()
messages.success('Action "{}" created successfully!'.format(instance.type)) messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url() next_url = self.get_next_url()
return flask.redirect(next_url) return flask.redirect(next_url)
@ -346,7 +356,9 @@ class NewAllocateView(NewActionView, DeviceListMix):
if self.form.validate_on_submit(): if self.form.validate_on_submit():
instance = self.form.save() instance = self.form.save()
messages.success('Action "{}" created successfully!'.format(instance.type)) messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url() next_url = self.get_next_url()
return flask.redirect(next_url) return flask.redirect(next_url)
@ -366,7 +378,9 @@ class NewDataWipeView(NewActionView, DeviceListMix):
if self.form.validate_on_submit(): if self.form.validate_on_submit():
instance = self.form.save() instance = self.form.save()
messages.success('Action "{}" created successfully!'.format(instance.type)) messages.success(
'Action "{}" created successfully!'.format(self.form.type.data)
)
next_url = self.get_next_url() next_url = self.get_next_url()
return flask.redirect(next_url) return flask.redirect(next_url)
@ -377,13 +391,61 @@ class NewDataWipeView(NewActionView, DeviceListMix):
return flask.render_template(self.template_name, **self.context) 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():
instance = 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
)
devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add')) 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( devices.add_url_rule(
'/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add') '/action/allocate/add/', view_func=NewAllocateView.as_view('allocate_add')
) )
devices.add_url_rule( devices.add_url_rule(
'/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add') '/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add')
) )
devices.add_url_rule(
'/lot/<string:lot_id>/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=DeviceListView.as_view('devicelist'))
devices.add_url_rule( devices.add_url_rule(
'/device/<string:id>/', view_func=DeviceDetailView.as_view('device_details') '/device/<string:id>/', view_func=DeviceDetailView.as_view('device_details')

View File

@ -1,12 +1,16 @@
$(document).ready(function() { $(document).ready(function() {
var show_allocate_form = $("#allocateModal").data('show-action-form'); var show_allocate_form = $("#allocateModal").data('show-action-form');
var show_datawipe_form = $("#datawipeModal").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") { if (show_allocate_form != "None") {
$("#allocateModal .btn-primary").show(); $("#allocateModal .btn-primary").show();
newAllocate(show_allocate_form); newAllocate(show_allocate_form);
} else if (show_datawipe_form != "None") { } else if (show_datawipe_form != "None") {
$("#datawipeModal .btn-primary").show(); $("#datawipeModal .btn-primary").show();
newDataWipe(show_datawipe_form); newDataWipe(show_datawipe_form);
} else if (show_trade_form != "None") {
$("#tradeLotModal .btn-primary").show();
newTrade(show_trade_form);
} else { } else {
$(".deviceSelect").on("change", deviceSelect); $(".deviceSelect").on("change", deviceSelect);
} }
@ -15,6 +19,7 @@ $(document).ready(function() {
function deviceSelect() { function deviceSelect() {
var devices_count = $(".deviceSelect").filter(':checked').length; var devices_count = $(".deviceSelect").filter(':checked').length;
get_device_list();
if (devices_count == 0) { if (devices_count == 0) {
$("#addingLotModal .pol").show(); $("#addingLotModal .pol").show();
$("#addingLotModal .btn-primary").hide(); $("#addingLotModal .btn-primary").hide();
@ -50,23 +55,48 @@ function deviceSelect() {
$("#datawipeModal .btn-primary").show(); $("#datawipeModal .btn-primary").show();
$("#addingTagModal .pol").hide(); $("#addingTagModal .pol").hide();
$("#addingTagModal .btn-primary").show();
} }
} }
function removeTag() { function removeTag() {
var devices = $(".deviceSelect").filter(':checked'); var devices = $(".deviceSelect").filter(':checked');
var devices_id = $.map(devices, function(x) { return $(x).attr('data')}); var devices_id = $.map(devices, function(x) { return $(x).attr('data')});
console.log(devices_id);
if (devices_id.length > 0) { if (devices_id.length > 0) {
var url = "/inventory/tag/devices/"+devices_id[0]+"/del/"; var url = "/inventory/tag/devices/"+devices_id[0]+"/del/";
window.location.href = url; 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) { function newAction(action) {
$("#actionModal #type").val(action); $("#actionModal #type").val(action);
$("#actionModal #title-action").html(action); $("#actionModal #title-action").html(action);
get_device_list();
deviceSelect(); deviceSelect();
$("#activeActionModal").click(); $("#activeActionModal").click();
} }
@ -74,7 +104,6 @@ function newAction(action) {
function newAllocate(action) { function newAllocate(action) {
$("#allocateModal #type").val(action); $("#allocateModal #type").val(action);
$("#allocateModal #title-action").html(action); $("#allocateModal #title-action").html(action);
get_device_list();
deviceSelect(); deviceSelect();
$("#activeAllocateModal").click(); $("#activeAllocateModal").click();
} }
@ -82,7 +111,6 @@ function newAllocate(action) {
function newDataWipe(action) { function newDataWipe(action) {
$("#datawipeModal #type").val(action); $("#datawipeModal #type").val(action);
$("#datawipeModal #title-action").html(action); $("#datawipeModal #title-action").html(action);
get_device_list();
deviceSelect(); deviceSelect();
$("#activeDatawipeModal").click(); $("#activeDatawipeModal").click();
} }

View File

@ -24,7 +24,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary d-none" value="Save changes" /> <input type="submit" class="btn btn-primary" style="display: none;" value="Save changes" />
</div> </div>
</form> </form>

View File

@ -1,5 +1,5 @@
<div class="modal fade" id="allocateModal" tabindex="-1" style="display: none;" aria-hidden="true" <div class="modal fade" id="allocateModal" tabindex="-1" style="display: none;" aria-hidden="true"
data-show-action-form="{{ form_new_allocate.type.data }}"> data-show-action-form="{{ form_new_allocate.check_valid() }}">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">

View File

@ -1,5 +1,5 @@
<div class="modal fade" id="datawipeModal" tabindex="-1" style="display: none;" aria-hidden="true" <div class="modal fade" id="datawipeModal" tabindex="-1" style="display: none;" aria-hidden="true"
data-show-action-form="{{ form_new_datawipe.type.data }}"> data-show-action-form="{{ form_new_datawipe.check_valid() }}">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">

View File

@ -42,185 +42,223 @@
<div class="card"> <div class="card">
<div class="card-body pt-3" style="min-height: 650px;"> <div class="card-body pt-3" style="min-height: 650px;">
<!-- Bordered Tabs --> <!-- Bordered Tabs -->
{% if lot and not lot.is_temporary %}
<ul class="nav nav-tabs nav-tabs-bordered">
<div class="btn-group dropdown ml-1"> <li class="nav-item">
{% if lot and lot.is_temporary and not lot.devices %} <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#devices-list">Devices</button>
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="modal" data-bs-target="#btnRemoveLots"> </li>
<i class="bi bi-trash"></i>
Remove Lot <li class="nav-item">
<span class="caret"></span> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#trade-documents-list">Documents</button>
</button> </li>
{% else %}
<button id="btnLots" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> </ul>
<i class="bi bi-folder2"></i> {% endif %}
Lots <div class="tab-content pt-5">
<span class="caret"></span> <div id="devices-list" class="tab-pane fade devices-list active show">
</button>
{% endif %} <div class="btn-group dropdown ml-1">
<ul class="dropdown-menu" aria-labelledby="btnLots"> <button id="btnLots" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<li> <i class="bi bi-folder2"></i>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addingLotModal"> Lots
<span class="caret"></span>
</button>
<span class="d-none" id="activeTradeModal" data-bs-toggle="modal" data-bs-target="#tradeLotModal"></span>
<ul class="dropdown-menu" aria-labelledby="btnLots">
{% if lot and lot.is_temporary and not lot.devices %}
<li>
<a href="javascript:newAction('Use')" class="dropdown-item"
data-bs-toggle="modal" data-bs-target="#btnRemoveLots">
<i class="bi bi-trash"></i>
Remove Lot
<span class="caret"></span>
</a>
</li>
{% endif %}
<li>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addingLotModal">
<i class="bi bi-plus"></i>
Add selected Devices to a lot
</a>
</li>
<li>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#removeLotModal">
<i class="bi bi-x"></i>
Remove selected devices from a lot
</a>
</li>
{% if lot.is_temporary %}
<li>
<a href="javascript:newTrade('user_from')" class="dropdown-item">
<i class="bi bi-plus"></i>
Add supplier
</a>
</li>
<li>
<a href="javascript:newTrade('user_to')" class="dropdown-item">
<i class="bi bi-plus"></i>
Add receiver
</a>
</li>
{% endif %}
{% if lot and not lot.is_temporary %}
<li>
<a href="{{ url_for('inventory.devices.trade_document_add', lot_id=lot.id)}}" class="dropdown-item">
<i class="bi bi-plus"></i>
Add new document
<span class="caret"></span>
</a>
</li>
{% endif %}
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Add selected Devices to a lot New Actions
</a> </button>
</li> <span class="d-none" id="activeActionModal" data-bs-toggle="modal" data-bs-target="#actionModal"></span>
<li> <span class="d-none" id="activeAllocateModal" data-bs-toggle="modal" data-bs-target="#allocateModal"></span>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#removeLotModal"> <span class="d-none" id="activeDatawipeModal" data-bs-toggle="modal" data-bs-target="#datawipeModal"></span>
<i class="bi bi-x"></i> <ul class="dropdown-menu" aria-labelledby="btnActions">
Remove selected devices from a lot <li>
</a> Status actions
</li> </li>
</ul> <li>
</div> <a href="javascript:newAction('Recycling')" class="dropdown-item">
<i class="bi bi-recycle"></i>
Recycling
</a>
</li>
<li>
<a href="javascript:newAction('Use')" class="dropdown-item">
<i class="bi bi-play-circle-fill"></i>
Use
</a>
</li>
<li>
<a href="javascript:newAction('Refurbish')" class="dropdown-item">
<i class="bi bi-tools"></i>
Refurbish
</a>
</li>
<li>
<a href="javascript:newAction('Management')" class="dropdown-item">
<i class="bi bi-mastodon"></i>
Management
</a>
</li>
<li>
Allocation
</li>
<li>
<a href="javascript:newAllocate('Allocate')" class="dropdown-item">
<i class="bi bi-house-fill"></i>
Allocate
</a>
</li>
<li>
<a href="javascript:newAllocate('Deallocate')" class="dropdown-item">
<i class="bi bi-house"></i>
Deallocate
</a>
</li>
<li>
Physical actions
</li>
<li>
<a href="javascript:newAction('ToPrepare')" class="dropdown-item">
<i class="bi bi-tools"></i>
ToPrepare
</a>
</li>
<li>
<a href="javascript:newAction('Prepare')" class="dropdown-item">
<i class="bi bi-egg"></i>
Prepare
</a>
</li>
<li>
<a href="javascript:newDataWipe('DataWipe')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
DataWipe
</a>
</li>
<li>
<a href="javascript:newAction('ToRepair')" class="dropdown-item">
<i class="bi bi-screwdriver"></i>
ToRepair
</a>
</li>
<li>
<a href="javascript:newAction('Ready')" class="dropdown-item">
<i class="bi bi-check2-all"></i>
Ready
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnActions" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-plus"></i> <i class="bi bi-reply"></i>
New Actions Exports
</button> </button>
<span class="d-none" id="activeActionModal" data-bs-toggle="modal" data-bs-target="#actionModal"></span> <ul class="dropdown-menu" aria-labelledby="btnExport">
<span class="d-none" id="activeAllocateModal" data-bs-toggle="modal" data-bs-target="#allocateModal"></span> <li>
<span class="d-none" id="activeDatawipeModal" data-bs-toggle="modal" data-bs-target="#datawipeModal"></span> <a href="#" class="dropdown-item">
<ul class="dropdown-menu" aria-labelledby="btnActions"> TODO: Not implemented
<li> </a>
Status actions </li>
</li> </ul>
<li> </div>
<a href="javascript:newAction('Recycling')" class="dropdown-item">
<i class="bi bi-recycle"></i>
Recycling
</a>
</li>
<li>
<a href="javascript:newAction('Use')" class="dropdown-item">
<i class="bi bi-play-circle-fill"></i>
Use
</a>
</li>
<li>
<a href="javascript:newAction('Refurbish')" class="dropdown-item">
<i class="bi bi-tools"></i>
Refurbish
</a>
</li>
<li>
<a href="javascript:newAction('Management')" class="dropdown-item">
<i class="bi bi-mastodon"></i>
Management
</a>
</li>
<li>
Allocation
</li>
<li>
<a href="javascript:newAllocate('Allocate')" class="dropdown-item">
<i class="bi bi-house-fill"></i>
Allocate
</a>
</li>
<li>
<a href="javascript:newAllocate('Deallocate')" class="dropdown-item">
<i class="bi bi-house"></i>
Deallocate
</a>
</li>
<li>
Physical actions
</li>
<li>
<a href="javascript:newAction('ToPrepare')" class="dropdown-item">
<i class="bi bi-tools"></i>
ToPrepare
</a>
</li>
<li>
<a href="javascript:newAction('Prepare')" class="dropdown-item">
<i class="bi bi-egg"></i>
Prepare
</a>
</li>
<li>
<a href="javascript:newDataWipe('DataWipe')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i>
DataWipe
</a>
</li>
<li>
<a href="javascript:newAction('ToRepair')" class="dropdown-item">
<i class="bi bi-screwdriver"></i>
ToRepair
</a>
</li>
<li>
<a href="javascript:newAction('Ready')" class="dropdown-item">
<i class="bi bi-check2-all"></i>
Ready
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnExport" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-reply"></i> <i class="bi bi-tag"></i>
Exports Tags
</button> </button>
<ul class="dropdown-menu" aria-labelledby="btnExport"> <ul class="dropdown-menu" aria-labelledby="btnTags">
<li> <li>
<a href="#" class="dropdown-item"> <a href="javascript:addTag()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addingTagModal">
TODO: Not implemented <i class="bi bi-plus"></i>
</a> Add Tag to selected Devices
</li> </a>
</ul> </li>
</div> <li>
<a href="javascript:removeTag()" class="dropdown-item">
<i class="bi bi-x"></i>
Remove Tag from selected Devices
</a>
</li>
</ul>
</div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="btn-group dropdown ml-1" uib-dropdown="">
<button id="btnTags" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i> <i class="bi bi-tag"></i>
Tags New Device
</button> </button>
<ul class="dropdown-menu" aria-labelledby="btnTags"> <ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li> <li>
<a href="javascript:void()" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addingTagModal"> <a href="{{ url_for('inventory.devices.upload_snapshot') }}" class="dropdown-item">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Add Tag to selected Devices Upload a new Snapshot
</a> </a>
</li> </li>
<li> <li>
<a href="javascript:removeTag()" class="dropdown-item"> <a href="{{ url_for('inventory.devices.device_add') }}" class="dropdown-item">
<i class="bi bi-x"></i> <i class="bi bi-plus"></i>
Remove Tag from selected Devices Create a new Device
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="btn-group dropdown ml-1" uib-dropdown=""> <div class="tab-content pt-2">
<button id="btnSnapshot" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-tag"></i>
New Device
</button>
<ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li>
<a href="{{ url_for('inventory.devices.upload_snapshot') }}" class="dropdown-item">
<i class="bi bi-plus"></i>
Upload a new Snapshot
</a>
</li>
<li>
<a href="{{ url_for('inventory.devices.device_add') }}" class="dropdown-item">
<i class="bi bi-plus"></i>
Create a new Device
</a>
</li>
</ul>
</div>
<div class="tab-content pt-2"> <h5 class="card-title">Computers</h5>
<div class="tab-pane fade show active profile-overview" id="profile-overview">
<h5 class="card-title">Computers</h5>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -267,8 +305,40 @@
</tbody> </tbody>
</table> </table>
</div><!-- End Bordered Tabs --> </div>
</div>
{% if lot and not lot.is_temporary %}
<div id="trade-documents-list" class="tab-pane fade trade-documents-list">
<h5 class="card-title">Documents</h5>
<table class="table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Uploaded on</th>
</tr>
</thead>
<tbody>
{% for doc in lot.trade.documents %}
<tr>
<td>
{% if doc.url.to_text() %}
<a href="{{ doc.url.to_text() }}" target="_blank">{{ doc.file_name}}</a>
{% else %}
{{ doc.file_name}}
{% endif %}
</td>
<td>
{{ doc.created.strftime('%H:%M %d-%m-%Y')}}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div><!-- End Bordered Tabs -->
</div>
</div> </div>
</div> </div>
@ -282,6 +352,7 @@
{% include "inventory/actions.html" %} {% include "inventory/actions.html" %}
{% include "inventory/allocate.html" %} {% include "inventory/allocate.html" %}
{% include "inventory/data_wipe.html" %} {% include "inventory/data_wipe.html" %}
{% include "inventory/trade.html" %}
<!-- CDN --> <!-- CDN -->
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest"></script> <script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest"></script>

View File

@ -0,0 +1,62 @@
{% if form_new_trade %}
<div class="modal fade" id="tradeLotModal" tabindex="-1" style="display: none;" aria-hidden="true"
data-show-action-form="{{ form_new_trade.check_valid() }}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">New Action <span id="title-action"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('inventory.devices.trade_add') }}" method="post">
{{ form_new_trade.csrf_token }}
<div class="modal-body">
{% for field in form_new_trade %}
{% if field != form_new_trade.csrf_token %}
{% if field == form_new_trade.devices %}
{{ field }}
{% elif field == form_new_trade.lot %}
{{ field }}
{% elif field == form_new_trade.type %}
{{ field }}
{% else %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{% if field == form_new_trade.confirm %}
<div class="form-check form-switch">
{{ field(class_="form-check-input") }}
<small class="text-muted">{{ field.description }}</small>
</div>
{% else %}
{{ field(class_="form-control") }}
<small class="text-muted">{{ field.description }}</small>
{% endif %}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<input type="submit" class="btn btn-primary" value="Create" />
</div>
</form>
</div>
</div>
</div>
{% else %}
<div class="modal fade" id="tradeLotModal" tabindex="-1" style="display: none;" aria-hidden="true"
data-show-action-form="None">
</div>
{% endif %}

View File

@ -0,0 +1,65 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
<nav>
<ol class="breadcrumb">
<!-- TODO@slamora replace with lot list URL when exists -->
<li class="breadcrumb-item"><a href="#TODO-lot-list">Lots</a></li>
<li class="breadcrumb-item">Trade Document</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-4">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">{{ title }}</h5>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<form action="{{ url_for('inventory.devices.trade_document_add', lot_id=form._lot.id) }}" method="post"
class="row g-3 needs-validation" enctype="multipart/form-data">
{{ form.csrf_token }}
{% for field in form %}
{% if field != form.csrf_token %}
<div class="col-12">
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% endif %}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endfor %}
<div class="col-12">
<a href="{{ url_for('inventory.devices.lotdevicelist', lot_id=form._lot.id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock main %}