Merge branch 'feature/3518-define-placeholder' of github.com:eReuse/devicehub-teal into feature/3518-define-placeholder

This commit is contained in:
Cayo Puigdefabregas 2022-07-12 11:25:58 +02:00
commit 9936a89171
20 changed files with 1571 additions and 63 deletions

View File

@ -3,6 +3,7 @@ import datetime
import json import json
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
import pandas as pd
from boltons.urlutils import URL from boltons.urlutils import URL
from flask import current_app as app from flask import current_app as app
from flask import g, request from flask import g, request
@ -29,6 +30,7 @@ from wtforms.fields import FormField
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.inventory.models import DeliveryNote, ReceiverNote, Transfer from ereuse_devicehub.inventory.models import DeliveryNote, ReceiverNote, Transfer
from ereuse_devicehub.parser.models import PlaceholdersLog
from ereuse_devicehub.parser.parser import ParseSnapshotLsHw from ereuse_devicehub.parser.parser import ParseSnapshotLsHw
from ereuse_devicehub.parser.schemas import Snapshot_lite from ereuse_devicehub.parser.schemas import Snapshot_lite
from ereuse_devicehub.resources.action.models import Snapshot, Trade from ereuse_devicehub.resources.action.models import Snapshot, Trade
@ -42,14 +44,17 @@ from ereuse_devicehub.resources.device.models import (
SAI, SAI,
Cellphone, Cellphone,
ComputerMonitor, ComputerMonitor,
Desktop,
Device, Device,
Keyboard, Keyboard,
Laptop,
MemoryCardReader, MemoryCardReader,
Mouse, Mouse,
Placeholder,
Server,
Smartphone, Smartphone,
Tablet, Tablet,
) )
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 from ereuse_devicehub.resources.enums import Severity
from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.hash_reports import insert_hash
@ -300,19 +305,27 @@ class UploadSnapshotForm(SnapshotMixin, FlaskForm):
class NewDeviceForm(FlaskForm): class NewDeviceForm(FlaskForm):
type = StringField('Type', [validators.DataRequired()]) type = StringField('Type', [validators.DataRequired()])
label = StringField('Label') amount = IntegerField(
serial_number = StringField('Seria Number', [validators.DataRequired()]) 'Amount',
model = StringField('Model', [validators.DataRequired()]) [validators.DataRequired(), validators.NumberRange(min=1, max=100)],
manufacturer = StringField('Manufacturer', [validators.DataRequired()]) default=1,
)
id_device_supplier = StringField('Id Supplier', [validators.Optional()])
phid = StringField('Placeholder Hardware identity (Phid)', [validators.Optional()])
pallet = StringField('Identity of pallet', [validators.Optional()])
info = TextAreaField('Info', [validators.Optional()])
serial_number = StringField('Seria Number', [validators.Optional()])
model = StringField('Model', [validators.Optional()])
manufacturer = StringField('Manufacturer', [validators.Optional()])
appearance = StringField('Appearance', [validators.Optional()]) appearance = StringField('Appearance', [validators.Optional()])
functionality = StringField('Functionality', [validators.Optional()]) functionality = StringField('Functionality', [validators.Optional()])
brand = StringField('Brand') brand = StringField('Brand')
generation = IntegerField('Generation') generation = IntegerField('Generation')
version = StringField('Version') version = StringField('Version')
weight = FloatField('Weight', [validators.DataRequired()]) weight = FloatField('Weight', [validators.Optional()])
width = FloatField('Width', [validators.DataRequired()]) width = FloatField('Width', [validators.Optional()])
height = FloatField('Height', [validators.DataRequired()]) height = FloatField('Height', [validators.Optional()])
depth = FloatField('Depth', [validators.DataRequired()]) depth = FloatField('Depth', [validators.Optional()])
variant = StringField('Variant', [validators.Optional()]) variant = StringField('Variant', [validators.Optional()])
sku = StringField('SKU', [validators.Optional()]) sku = StringField('SKU', [validators.Optional()])
image = StringField('Image', [validators.Optional(), validators.URL()]) image = StringField('Image', [validators.Optional(), validators.URL()])
@ -322,8 +335,16 @@ class NewDeviceForm(FlaskForm):
screen = FloatField('Screen size', [validators.Optional()]) screen = FloatField('Screen size', [validators.Optional()])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._obj = kwargs.pop('_obj', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self._obj:
self.type.data = self._obj.type
if not request.form:
self.reset_from_obj()
self.devices = { self.devices = {
"Laptop": Laptop,
"Desktop": Desktop,
"Server": Server,
"Smartphone": Smartphone, "Smartphone": Smartphone,
"Tablet": Tablet, "Tablet": Tablet,
"Cellphone": Cellphone, "Cellphone": Cellphone,
@ -349,6 +370,45 @@ class NewDeviceForm(FlaskForm):
if not self.depth.data: if not self.depth.data:
self.depth.data = 0.1 self.depth.data = 0.1
def reset_from_obj(self):
if not self._obj:
return
disabled = {'disabled': "disabled"}
appearance = self._obj.appearance()
functionality = self._obj.functionality()
if appearance:
appearance = appearance.name
if functionality:
functionality = functionality.name
self.type.render_kw = disabled
self.type.data = self._obj.type
self.amount.render_kw = disabled
self.id_device_supplier.data = self._obj.placeholder.id_device_supplier
self.phid.data = self._obj.placeholder.phid
self.pallet.data = self._obj.placeholder.pallet
self.info.data = self._obj.placeholder.info
self.serial_number.data = self._obj.serial_number
self.model.data = self._obj.model
self.manufacturer.data = self._obj.manufacturer
self.appearance.data = appearance
self.functionality.data = functionality
self.brand.data = self._obj.brand
self.generation.data = self._obj.generation
self.version.data = self._obj.version
self.weight.data = self._obj.weight
self.width.data = self._obj.width
self.height.data = self._obj.height
self.depth.data = self._obj.depth
self.variant.data = self._obj.variant
self.sku.data = self._obj.sku
self.image.data = self._obj.image
if self._obj.type in ['Smartphone', 'Tablet', 'Cellphone']:
self.imei.data = self._obj.imei
self.meid.data = self._obj.meid
if self._obj.type == 'ComputerMonitor':
self.resolution.data = self._obj.resolution_width
self.screen.data = self._obj.size
def validate(self, extra_validators=None): # noqa: C901 def validate(self, extra_validators=None): # noqa: C901
error = ["Not a correct value"] error = ["Not a correct value"]
is_valid = super().validate(extra_validators) is_valid = super().validate(extra_validators)
@ -373,12 +433,12 @@ class NewDeviceForm(FlaskForm):
self.depth.errors = error self.depth.errors = error
is_valid = False is_valid = False
if self.imei.data: if self.imei.data and self.amount.data == 1:
if not 13 < len(str(self.imei.data)) < 17: if not 13 < len(str(self.imei.data)) < 17:
self.imei.errors = error self.imei.errors = error
is_valid = False is_valid = False
if self.meid.data: if self.meid.data and self.amount.data == 1:
meid = self.meid.data meid = self.meid.data
if not 13 < len(meid) < 17: if not 13 < len(meid) < 17:
is_valid = False is_valid = False
@ -388,6 +448,28 @@ class NewDeviceForm(FlaskForm):
self.meid.errors = error self.meid.errors = error
is_valid = False is_valid = False
if self.phid.data and self.amount.data == 1 and not self._obj:
dev = Placeholder.query.filter(
Placeholder.phid == self.phid.data, Device.owner == g.user
).first()
if dev:
msg = "Sorry, exist one snapshot device with this HID"
self.phid.errors = [msg]
is_valid = False
if (
self.phid.data
and self._obj
and self.phid.data != self._obj.placeholder.phid
):
dev = Placeholder.query.filter(
Placeholder.phid == self.phid.data, Device.owner == g.user
).first()
if dev:
msg = "Sorry, exist one snapshot device with this HID"
self.phid.errors = [msg]
is_valid = False
if not is_valid: if not is_valid:
return False return False
@ -403,7 +485,18 @@ class NewDeviceForm(FlaskForm):
return True return True
def save(self, commit=True): def save(self, commit=True):
if self._obj:
self.edit_device()
else:
for n in range(self.amount.data):
self.reset_ids()
self.create_device()
if commit:
db.session.commit()
def create_device(self):
schema = SnapshotSchema()
json_snapshot = { json_snapshot = {
'type': 'Snapshot', 'type': 'Snapshot',
'software': 'Web', 'software': 'Web',
@ -434,33 +527,84 @@ class NewDeviceForm(FlaskForm):
'functionalityRange': self.functionality.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) snapshot_json = schema.load(json_snapshot)
device = snapshot_json['device']
if self.type.data == 'ComputerMonitor': if self.type.data == 'ComputerMonitor':
snapshot_json['device'].resolution_width = self.resolution.data device.resolution_width = self.resolution.data
snapshot_json['device'].size = self.screen.data device.size = self.screen.data
if self.type.data in ['Smartphone', 'Tablet', 'Cellphone']: if self.type.data in ['Smartphone', 'Tablet', 'Cellphone']:
snapshot_json['device'].imei = self.imei.data device.imei = self.imei.data
snapshot_json['device'].meid = self.meid.data device.meid = self.meid.data
snapshot = upload_form.build(snapshot_json) device.placeholder = self.get_placeholder()
db.session.add(device)
move_json(self.tmp_snapshots, path_snapshot, g.user.email) placeholder_log = PlaceholdersLog(
if self.type.data == 'ComputerMonitor': type="New device", source='Web form', placeholder=device.placeholder
snapshot.device.resolution = self.resolution.data )
snapshot.device.screen = self.screen.data db.session.add(placeholder_log)
if commit: def reset_ids(self):
db.session.commit() if self.amount.data > 1:
return snapshot self.phid.data = None
self.id_device_supplier.data = None
self.serial_number.data = None
self.sku.data = None
self.imei.data = None
self.meid.data = None
def get_placeholder(self):
self.placeholder = Placeholder(
**{
'phid': self.phid.data or None,
'id_device_supplier': self.id_device_supplier.data,
'info': self.info.data,
'pallet': self.pallet.data,
}
)
return self.placeholder
def edit_device(self):
self._obj.placeholder.phid = self.phid.data or self._obj.placeholder.phid
self._obj.placeholder.id_device_supplier = self.id_device_supplier.data or None
self._obj.placeholder.info = self.info.data or None
self._obj.placeholder.pallet = self.pallet.data or None
self._obj.model = self.model.data
self._obj.manufacturer = self.manufacturer.data
self._obj.serial_number = self.serial_number.data
self._obj.brand = self.brand.data
self._obj.version = self.version.data
self._obj.generation = self.generation.data
self._obj.sku = self.sku.data
self._obj.weight = self.weight.data
self._obj.width = self.width.data
self._obj.height = self.height.data
self._obj.depth = self.depth.data
self._obj.variant = self.variant.data
self._obj.image = self.image.data
if self._obj.type == 'ComputerMonitor':
self._obj.resolution_width = self.resolution.data
self._obj.size = self.screen.data
if self._obj.type in ['Smartphone', 'Tablet', 'Cellphone']:
self._obj.imei = self.imei.data
self._obj.meid = self.meid.data
if self.appearance.data and self.appearance.data != self._obj.appearance().name:
self._obj.set_appearance(self.appearance.data)
if (
self.functionality.data
and self.functionality.data != self._obj.functionality().name
):
self._obj.set_functionality(self.functionality.data)
placeholder_log = PlaceholdersLog(
type="Update", source='Web form', placeholder=self._obj.placeholder
)
db.session.add(placeholder_log)
class TagDeviceForm(FlaskForm): class TagDeviceForm(FlaskForm):
@ -1295,3 +1439,164 @@ class NotesForm(FlaskForm):
db.session.commit() db.session.commit()
return self._obj return self._obj
class UploadPlaceholderForm(FlaskForm):
type = StringField('Type', [validators.DataRequired()])
placeholder_file = FileField(
'Select a Placeholder File', [validators.DataRequired()]
)
def get_data_file(self):
files = request.files.getlist(self.placeholder_file.name)
if not files:
return False
_file = files[0]
if _file.content_type == 'text/csv':
self.source = "CSV File: {}".format(_file.filename)
delimiter = ';'
data = pd.read_csv(_file).to_dict()
head = list(data.keys())[0].split(delimiter)
values = [
{k: v.split(delimiter)} for x in data.values() for k, v in x.items()
]
data = {}
for i in range(len(head)):
data[head[i]] = {}
for x in values:
for k, v in x.items():
data[head[i]][k] = v[i]
else:
self.source = "Excel File: {}".format(_file.filename)
try:
data = pd.read_excel(_file).to_dict()
except ValueError:
self.placeholder_file.errors = ["File don't have a correct format"]
return False
return data
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not is_valid:
return False
if not request.files.getlist(self.placeholder_file.name):
return False
data = self.get_data_file()
if not data:
return False
header = [
'Phid',
'Model',
'Manufacturer',
'Serial Number',
'Id device Supplier',
'Pallet',
'Info',
]
for k in header:
if k not in data.keys():
self.placeholder_file.errors = ["Missing required fields in the file"]
return False
self.placeholders = []
schema = SnapshotSchema()
self.path_snapshots = {}
for i in data['Phid'].keys():
placeholder = None
if data['Phid'][i]:
placeholder = Placeholder.query.filter_by(phid=data['Phid'][i]).first()
# update one
if placeholder:
device = placeholder.device
device.model = "{}".format(data['Model'][i]).lower()
device.manufacturer = "{}".format(data['Manufacturer'][i]).lower()
device.serial_number = "{}".format(data['Serial Number'][i]).lower()
placeholder.id_device_supplier = "{}".format(
data['Id device Supplier'][i]
)
placeholder.pallet = "{}".format(data['Pallet'][i])
placeholder.info = "{}".format(data['Info'][i])
placeholder_log = PlaceholdersLog(
type="Update", source=self.source, placeholder=device.placeholder
)
self.placeholders.append((device, placeholder_log))
continue
# create a new one
json_snapshot = {
'type': 'Snapshot',
'software': 'Web',
'version': '11.0',
'device': {
'type': self.type.data,
'model': "{}".format(data['Model'][i]),
'manufacturer': "{}".format(data['Manufacturer'][i]),
'serialNumber': "{}".format(data['Serial Number'][i]),
},
}
json_placeholder = {
'phid': data['Phid'][i] or None,
'id_device_supplier': data['Id device Supplier'][i],
'pallet': data['Pallet'][i],
'info': data['Info'][i],
}
snapshot_json = schema.load(json_snapshot)
device = snapshot_json['device']
device.placeholder = Placeholder(**json_placeholder)
placeholder_log = PlaceholdersLog(
type="New device", source=self.source, placeholder=device.placeholder
)
self.placeholders.append((device, placeholder_log))
return True
def save(self, commit=True):
for device, placeholder_log in self.placeholders:
db.session.add(device)
db.session.add(placeholder_log)
if commit:
db.session.commit()
return self.placeholders
class EditPlaceholderForm(FlaskForm):
manufacturer = StringField('Manufacturer', [validators.Optional()])
model = StringField('Model', [validators.Optional()])
serial_number = StringField('Serial Number', [validators.Optional()])
id_device_supplier = StringField('Id Supplier', [validators.Optional()])
phid = StringField('Phid', [validators.DataRequired()])
pallet = StringField('Pallet', [validators.Optional()])
info = StringField('Info', [validators.Optional()])
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if not is_valid:
return False
return True
def save(self, commit=True):
for device in self.placeholders:
db.session.add(device)
if commit:
db.session.commit()
return self.placeholders

View File

@ -30,10 +30,11 @@ from ereuse_devicehub.inventory.forms import (
TradeDocumentForm, TradeDocumentForm,
TradeForm, TradeForm,
TransferForm, TransferForm,
UploadPlaceholderForm,
UploadSnapshotForm, UploadSnapshotForm,
) )
from ereuse_devicehub.labels.forms import PrintLabelsForm from ereuse_devicehub.labels.forms import PrintLabelsForm
from ereuse_devicehub.parser.models import SnapshotsLog from ereuse_devicehub.parser.models import PlaceholdersLog, SnapshotsLog
from ereuse_devicehub.resources.action.models import Trade from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
@ -272,10 +273,37 @@ class DeviceCreateView(GenericMixin):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class DeviceEditView(GenericMixin):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/device_create.html'
def dispatch_request(self, id):
self.get_context()
device = (
Device.query.filter(Device.owner_id == current_user.id)
.filter(Device.devicehub_id == id)
.one()
)
form = NewDeviceForm(_obj=device)
self.context.update(
{
'page_title': 'Edit Device',
'form': form,
}
)
if form.validate_on_submit():
next_url = url_for('inventory.device_details', id=id)
form.save(commit=True)
messages.success('Device "{}" edited successfully!'.format(form.type.data))
return flask.redirect(next_url)
return flask.render_template(self.template_name, **self.context)
class TagLinkDeviceView(View): class TagLinkDeviceView(View):
methods = ['POST'] methods = ['POST']
decorators = [login_required] decorators = [login_required]
# template_name = 'inventory/device_list.html'
def dispatch_request(self): def dispatch_request(self):
form = TagDeviceForm() form = TagDeviceForm()
@ -832,6 +860,53 @@ class ReceiverNoteView(GenericMixin):
return flask.redirect(next_url) return flask.redirect(next_url)
class UploadPlaceholderView(GenericMixin):
methods = ['GET', 'POST']
decorators = [login_required]
template_name = 'inventory/upload_placeholder.html'
def dispatch_request(self, lot_id=None):
self.get_context()
form = UploadPlaceholderForm()
self.context.update(
{
'page_title': 'Upload Placeholder',
'form': form,
'lot_id': lot_id,
}
)
if form.validate_on_submit():
snapshots = form.save(commit=False)
if lot_id:
lots = self.context['lots']
lot = lots.filter(Lot.id == lot_id).one()
for device, p in snapshots:
lot.devices.add(device)
db.session.add(lot)
db.session.commit()
messages.success('Placeholders uploaded successfully!')
return flask.render_template(self.template_name, **self.context)
class PlaceholderLogListView(GenericMixin):
template_name = 'inventory/placeholder_log_list.html'
def dispatch_request(self):
self.get_context()
self.context['page_title'] = "Placeholder Logs"
self.context['placeholders_log'] = self.get_placeholders_log()
return flask.render_template(self.template_name, **self.context)
def get_placeholders_log(self):
placeholder_log = PlaceholdersLog.query.filter(
PlaceholdersLog.owner == g.user
).order_by(PlaceholdersLog.created.desc())
return placeholder_log
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('/action/trade/add/', view_func=NewTradeView.as_view('trade_add'))
devices.add_url_rule( devices.add_url_rule(
@ -871,6 +946,9 @@ devices.add_url_rule(
'/lot/<string:lot_id>/device/add/', '/lot/<string:lot_id>/device/add/',
view_func=DeviceCreateView.as_view('lot_device_add'), view_func=DeviceCreateView.as_view('lot_device_add'),
) )
devices.add_url_rule(
'/device/edit/<string:id>/', view_func=DeviceEditView.as_view('device_edit')
)
devices.add_url_rule( devices.add_url_rule(
'/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add') '/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add')
) )
@ -902,3 +980,14 @@ devices.add_url_rule(
'/lot/<string:lot_id>/receivernote/', '/lot/<string:lot_id>/receivernote/',
view_func=ReceiverNoteView.as_view('receiver_note'), view_func=ReceiverNoteView.as_view('receiver_note'),
) )
devices.add_url_rule(
'/upload-placeholder/',
view_func=UploadPlaceholderView.as_view('upload_placeholder'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/upload-placeholder/',
view_func=UploadPlaceholderView.as_view('lot_upload_placeholder'),
)
devices.add_url_rule(
'/placeholder-logs/', view_func=PlaceholderLogListView.as_view('placeholder_logs')
)

View File

@ -0,0 +1,65 @@
"""placeholder log
Revision ID: 3e3a67f62972
Revises: aeca9fb50cc6
Create Date: 2022-07-06 18:23:54.267003
"""
import citext
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '3e3a67f62972'
down_revision = 'aeca9fb50cc6'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
op.create_table(
'placeholders_log',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='The last time Devicehub recorded a change for \n this thing.\n ',
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='When Devicehub created this.',
),
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('source', citext.CIText(), nullable=True),
sa.Column('type', citext.CIText(), nullable=True),
sa.Column('severity', sa.SmallInteger(), nullable=False),
sa.Column('placeholder_id', sa.BigInteger(), nullable=True),
sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(
['placeholder_id'],
['placeholder.id'],
),
sa.ForeignKeyConstraint(
['owner_id'],
['common.user.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.execute("CREATE SEQUENCE placeholders_log_seq START 1;")
def downgrade():
op.drop_table('placeholders_log')
op.execute("DROP SEQUENCE placeholders_log_seq;")

View File

@ -0,0 +1,57 @@
"""add placeholder
Revision ID: aeca9fb50cc6
Revises: 8d4fe4b497b3
Create Date: 2022-06-27 13:09:30.497678
"""
import citext
import sqlalchemy as sa
from alembic import context, op
# revision identifiers, used by Alembic.
revision = 'aeca9fb50cc6'
down_revision = '8d4fe4b497b3'
branch_labels = None
depends_on = None
def get_inv():
INV = context.get_x_argument(as_dictionary=True).get('inventory')
if not INV:
raise ValueError("Inventory value is not specified")
return INV
def upgrade():
# creating placeholder table
op.create_table(
'placeholder',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
),
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('phid', sa.Unicode(), nullable=False),
sa.Column('id_device_supplier', sa.Unicode(), nullable=True),
sa.Column('pallet', sa.Unicode(), nullable=True),
sa.Column('info', citext.CIText(), nullable=True),
sa.Column('device_id', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id']),
sa.PrimaryKeyConstraint('id'),
)
op.execute("CREATE SEQUENCE placeholder_seq START 1;")
def downgrade():
op.drop_table('placeholder')
op.execute("DROP SEQUENCE placeholder_seq;")

View File

@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import UUID
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import Snapshot from ereuse_devicehub.resources.action.models import Snapshot
from ereuse_devicehub.resources.device.models import Placeholder
from ereuse_devicehub.resources.enums import Severity from ereuse_devicehub.resources.enums import Severity
from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.models import Thing
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
@ -43,3 +44,48 @@ class SnapshotsLog(Thing):
return self.snapshot.device.devicehub_id return self.snapshot.device.devicehub_id
return '' return ''
class PlaceholdersLog(Thing):
"""A Placeholder log."""
__table_args__ = {'schema': ''}
id = Column(BigInteger, Sequence('placeholders_log_seq'), primary_key=True)
source = Column(CIText(), default='', nullable=True)
type = Column(CIText(), default='', nullable=True)
severity = Column(SmallInteger, default=Severity.Info, nullable=False)
placeholder_id = Column(BigInteger, db.ForeignKey(Placeholder.id), nullable=True)
placeholder = db.relationship(
Placeholder, primaryjoin=placeholder_id == Placeholder.id
)
owner_id = db.Column(
UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id,
)
owner = db.relationship(User, primaryjoin=owner_id == User.id)
def save(self, commit=False):
db.session.add(self)
if commit:
db.session.commit()
@property
def phid(self):
if self.placeholder:
return self.placeholder.phid
return ''
@property
def dhid(self):
if self.placeholder:
return self.placeholder.device.devicehub_id
return ''
def get_status(self):
return Severity(self.severity)

View File

@ -75,6 +75,13 @@ def create_code(context):
return hashcode.encode(_id) return hashcode.encode(_id)
def create_phid(context):
_hid = Placeholder.query.order_by(Placeholder.id.desc()).first()
if _hid:
return str(_hid.id + 1)
return '1'
class Device(Thing): class Device(Thing):
"""Base class for any type of physical object that can be identified. """Base class for any type of physical object that can be identified.
@ -594,6 +601,34 @@ class Device(Thing):
args[POLYMORPHIC_ON] = cls.type args[POLYMORPHIC_ON] = cls.type
return args return args
def appearance(self):
actions = copy.copy(self.actions)
actions.sort(key=lambda x: x.created)
with suppress(LookupError, ValueError, StopIteration):
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
return action.appearance_range
def functionality(self):
actions = copy.copy(self.actions)
actions.sort(key=lambda x: x.created)
with suppress(LookupError, ValueError, StopIteration):
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
return action.functionality_range
def set_appearance(self, value):
actions = copy.copy(self.actions)
actions.sort(key=lambda x: x.created)
with suppress(LookupError, ValueError, StopIteration):
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
action.appearance_range = value
def set_functionality(self, value):
actions = copy.copy(self.actions)
actions.sort(key=lambda x: x.created)
with suppress(LookupError, ValueError, StopIteration):
action = next(e for e in reversed(actions) if e.type == 'VisualTest')
action.functionality_range = value
def is_status(self, action): def is_status(self, action):
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
@ -791,6 +826,32 @@ class DisplayMixin:
return v return v
class Placeholder(Thing):
__table_args__ = {'schema': ''}
id = Column(BigInteger, Sequence('placeholder_seq'), primary_key=True)
pallet = Column(Unicode(), nullable=True)
phid = Column(Unicode(), nullable=False, default=create_phid)
pallet.comment = "used for identification where from where is this placeholders"
info = db.Column(CIText())
info.comment = "more info of placeholders"
id_device_supplier = db.Column(CIText())
id_device_supplier.comment = (
"Identification used for one supplier of one placeholders"
)
device_id = db.Column(
BigInteger,
db.ForeignKey(Device.id),
nullable=False,
)
device = db.relationship(
Device,
backref=backref('placeholder', lazy=True, uselist=False),
primaryjoin=device_id == Device.id,
)
device_id.comment = "datas of the placeholder"
class Computer(Device): class Computer(Device):
"""A chassis with components inside that can be processed """A chassis with components inside that can be processed
automatically with Workbench Computer. automatically with Workbench Computer.

View File

@ -18,6 +18,7 @@ from ereuse_devicehub.resources.device.models import (
Computer, Computer,
DataStorage, DataStorage,
Device, Device,
Placeholder,
) )
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
@ -195,6 +196,7 @@ class Sync:
db_device = Device.query.filter_by( db_device = Device.query.filter_by(
hid=device.hid, owner_id=g.user.id, active=True hid=device.hid, owner_id=g.user.id, active=True
).one() ).one()
if db_device and db_device.allocated: if db_device and db_device.allocated:
raise ResourceNotFound('device is actually allocated {}'.format(device)) raise ResourceNotFound('device is actually allocated {}'.format(device))
@ -226,6 +228,7 @@ class Sync:
device.physical_properties, device.physical_properties,
) )
db_device = sample_tag.device db_device = sample_tag.device
if db_device: # Device from hid or tags if db_device: # Device from hid or tags
self.merge(device, db_device) self.merge(device, db_device)
else: # Device is new and tags are not linked to a device else: # Device is new and tags are not linked to a device
@ -261,6 +264,9 @@ class Sync:
if db_device.owner_id != g.user.id: if db_device.owner_id != g.user.id:
return return
if device.placeholder and not db_device.placeholder:
return
for field_name, value in device.physical_properties.items(): for field_name, value in device.physical_properties.items():
if value is not None: if value is not None:
setattr(db_device, field_name, value) setattr(db_device, field_name, value)
@ -272,6 +278,23 @@ class Sync:
if hasattr(device, 'system_uuid') and device.system_uuid: if hasattr(device, 'system_uuid') and device.system_uuid:
db_device.system_uuid = device.system_uuid db_device.system_uuid = device.system_uuid
if device.placeholder and db_device.placeholder:
db_device.placeholder.pallet = device.placeholder.pallet
db_device.placeholder.info = device.placeholder.info
db_device.placeholder.id_device_supplier = (
device.placeholder.id_device_supplier
)
db_device.sku = device.sku
db_device.image = device.image
db_device.brand = device.brand
db_device.generation = device.generation
db_device.variant = device.variant
db_device.version = device.version
db_device.width = device.width
db_device.height = device.height
db_device.depth = device.depth
db_device.weight = device.weight
@staticmethod @staticmethod
def add_remove(device: Computer, components: Set[Component]) -> OrderedSet: def add_remove(device: Computer, components: Set[Component]) -> OrderedSet:
"""Generates the Add and Remove actions (but doesn't add them to """Generates the Add and Remove actions (but doesn't add them to

View File

@ -1,6 +1,8 @@
$(document).ready(() => { $(document).ready(() => {
$("#type").on("change", deviceInputs); $("#type").on("change", deviceInputs);
$("#amount").on("change", amountInputs);
deviceInputs(); deviceInputs();
amountInputs();
}) })
function deviceInputs() { function deviceInputs() {
@ -19,5 +21,23 @@ function deviceInputs() {
$("#resolution").hide(); $("#resolution").hide();
$("#imei").hide(); $("#imei").hide();
$("#meid").hide(); $("#meid").hide();
} };
amountInputs();
}
function amountInputs() {
if ($("#amount").val() > 1) {
$("#Phid").hide();
$("#Id_device_supplier").hide();
$("#Serial_number").hide();
$("#Sku").hide();
$("#imei").hide();
$("#meid").hide();
} else {
$("#Phid").show();
$("#Id_device_supplier").show();
$("#Serial_number").show();
$("#Sku").show();
deviceInputs();
};
} }

View File

@ -148,6 +148,15 @@
</a> </a>
</li> </li>
<li class="nav-heading">Placeholders</li>
<li class="nav-item">
<a class="nav-link collapsed" href="{{ url_for('inventory.placeholder_logs') }}">
<i class="bi-menu-button-wide"></i>
<span>Uploaded Placeholders</span>
</a>
</li>
<li class="nav-heading">Devices</li> <li class="nav-heading">Devices</li>
<li class="nav-item"> <li class="nav-item">

View File

@ -34,8 +34,16 @@
<div> <div>
<div class="form-group has-validation mb-2"> <div class="form-group has-validation mb-2">
<label for="name" class="form-label">Type *</label> <label for="name" class="form-label">Type *</label>
<select id="type" class="form-control" name="type" required=""> <select id="type" class="form-control" name="type" required="" {% if form.type.render_kw.disabled %}disabled="disabled"{% endif %}>
<option value="">Select one Type</option> <option value="">Select one Type</option>
<optgroup label="Computer">
<option value="Laptop"
{% if form.type.data == 'Laptop' %} selected="selected"{% endif %}>Laptop</option>
<option value="Desktop"
{% if form.type.data == 'Desktop' %} selected="selected"{% endif %}>Desktop</option>
<option value="Server"
{% if form.type.data == 'Server' %} selected="selected"{% endif %}>Server</option>
</optgroup>
<optgroup label="Monitor"> <optgroup label="Monitor">
<option value="ComputerMonitor" <option value="ComputerMonitor"
{% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Computer Monitor</option> {% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Computer Monitor</option>
@ -70,20 +78,72 @@
</div> </div>
<div class="form-group mb-2"> <div class="form-group mb-2">
<label for="label" class="form-label">Label</label> <label for="label" class="form-label">{{ form.amount.label }} *</label>
{{ form.label(class_="form-control") }} {{ form.amount(class_="form-control") }}
<small class="text-muted form-text">Label that you want link to this device</small> <small class="text-muted form-text">Number of devices you want to create of this type</small>
{% if form.label.errors %} {% if form.amount.errors %}
<p class="text-danger"> <p class="text-danger">
{% for error in form.label.errors %} {% for error in form.amount.errors %}
{{ error }}<br/> {{ error }}<br/>
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
</div> </div>
<div class="from-group has-validation mb-2"> <div class="form-group mb-2" id="Phid">
<label for="serialNumber" class="form-label">{{ form.serial_number.label }} *</label> <label for="label" class="form-label">{{ form.phid.label }}</label>
{{ form.phid(class_="form-control") }}
<small class="text-muted form-text">Label that you want link to this device</small>
{% if form.phid.errors %}
<p class="text-danger">
{% for error in form.phid.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="form-group mb-2" id="Id_device_supplier">
<label for="label" class="form-label">{{ form.id_device_supplier.label }}</label>
{{ form.id_device_supplier(class_="form-control") }}
<small class="text-muted form-text">Identity of device for the Supplier</small>
{% if form.id_device_supplier.errors %}
<p class="text-danger">
{% for error in form.id_device_supplier.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="form-group mb-2">
<label for="label" class="form-label">{{ form.pallet.label }}</label>
{{ form.pallet(class_="form-control") }}
<small class="text-muted form-text">Identity of pallet</small>
{% if form.pallet.errors %}
<p class="text-danger">
{% for error in form.pallet.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="form-group mb-2">
<label for="label" class="form-label">{{ form.info.label }}</label>
{{ form.info(class_="form-control") }}
<small class="text-muted form-text">Info extra</small>
{% if form.info.errors %}
<p class="text-danger">
{% for error in form.info.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="from-group has-validation mb-2" id="Serial_number">
<label for="serialNumber" class="form-label">{{ form.serial_number.label }}</label>
{{ form.serial_number(class_="form-control") }} {{ form.serial_number(class_="form-control") }}
<small class="text-muted form-text">Serial number of this device</small> <small class="text-muted form-text">Serial number of this device</small>
{% if form.serial_number.errors %} {% if form.serial_number.errors %}
@ -96,7 +156,7 @@
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.model.label }} *</label> <label for="model" class="form-label">{{ form.model.label }}</label>
{{ form.model(class_="form-control") }} {{ form.model(class_="form-control") }}
<small class="text-muted form-text">Name of model</small> <small class="text-muted form-text">Name of model</small>
{% if form.model.errors %} {% if form.model.errors %}
@ -109,7 +169,7 @@
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.manufacturer.label }} *</label> <label for="model" class="form-label">{{ form.manufacturer.label }}</label>
{{ form.manufacturer(class_="form-control") }} {{ form.manufacturer(class_="form-control") }}
<small class="text-muted form-text">Name of manufacturer</small> <small class="text-muted form-text">Name of manufacturer</small>
{% if form.manufacturer.errors %} {% if form.manufacturer.errors %}
@ -124,27 +184,33 @@
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.appearance.label }}</label> <label for="model" class="form-label">{{ form.appearance.label }}</label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="Z"> <input class="form-check-input" type="radio" name="appearance" value="Z"
{% if form.appearance.data == 'Z' %}checked="cheked"{% endif %}>
<label class="form-check-label">0. The device is new.</label> <label class="form-check-label">0. The device is new.</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="A"> <input class="form-check-input" type="radio" name="appearance" value="A"
{% if form.appearance.data == 'A' %}checked="checked"{% endif %}>
<label class="form-check-label">A. Like new (no visual damage))</label> <label class="form-check-label">A. Like new (no visual damage))</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="B"> <input class="form-check-input" type="radio" name="appearance" value="B"
{% if form.appearance.data == 'B' %}checked="checked"{% endif %}>
<label class="form-check-label">B. In very good condition (small visual damage to hard-to-detect parts)</label> <label class="form-check-label">B. In very good condition (small visual damage to hard-to-detect parts)</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="C"> <input class="form-check-input" type="radio" name="appearance" value="C"
{% if form.appearance.data == 'C' %}checked="checked"{% endif %}>
<label class="form-check-label">C. In good condition (small visual damage to easy-to-detect parts, not the screen))</label> <label class="form-check-label">C. In good condition (small visual damage to easy-to-detect parts, not the screen))</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="D"> <input class="form-check-input" type="radio" name="appearance" value="D"
{% if form.appearance.data == 'D' %}checked="checked"{% endif %}>
<label class="form-check-label">D. It is acceptable (visual damage to visible parts, not on the screen)</label> <label class="form-check-label">D. It is acceptable (visual damage to visible parts, not on the screen)</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="appearance" value="E"> <input class="form-check-input" type="radio" name="appearance" value="E"
{% if form.appearance.data == 'E' %}checked="checked"{% endif %}>
<label class="form-check-label">E. It is unacceptable (substantial visual damage that may affect use)</label> <label class="form-check-label">E. It is unacceptable (substantial visual damage that may affect use)</label>
</div> </div>
<small class="text-muted form-text">Rate the imperfections that affect the device aesthetically, but not its use.</small> <small class="text-muted form-text">Rate the imperfections that affect the device aesthetically, but not its use.</small>
@ -160,19 +226,23 @@
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.functionality.label }}</label> <label for="model" class="form-label">{{ form.functionality.label }}</label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="A"> <input class="form-check-input" type="radio" name="functionality" value="A"
{% if form.functionality.data == 'A' %}checked="checked"{% endif %}>
<label class="form-check-label">A. Everything works perfectly (buttons, and no scratches on the screen)</label> <label class="form-check-label">A. Everything works perfectly (buttons, and no scratches on the screen)</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="B"> <input class="form-check-input" type="radio" name="functionality" value="B"
{% if form.functionality.data == 'B' %}checked="checked"{% endif %}>
<label class="form-check-label">B. There is a hard to press button or small scratches on the corners of the screen</label> <label class="form-check-label">B. There is a hard to press button or small scratches on the corners of the screen</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="C"> <input class="form-check-input" type="radio" name="functionality" value="C"
{% if form.functionality.data == 'C' %}checked="checked"{% endif %}>
<label class="form-check-label">C. A non-essential button does not work; the screen has multiple scratches on the corners</label> <label class="form-check-label">C. A non-essential button does not work; the screen has multiple scratches on the corners</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="functionality" value="D"> <input class="form-check-input" type="radio" name="functionality" value="D"
{% if form.functionality.data == 'D' %}checked="checked"{% endif %}>
<label class="form-check-label">D. Multiple buttons do not work properly; the screen has severe damage that may affect use</label> <label class="form-check-label">D. Multiple buttons do not work properly; the screen has severe damage that may affect use</label>
</div> </div>
<small class="text-muted form-text">It qualifies the defects of a device that affect its use.</small> <small class="text-muted form-text">It qualifies the defects of a device that affect its use.</small>
@ -199,7 +269,7 @@
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.generation.label }} *</label> <label for="model" class="form-label">{{ form.generation.label }}</label>
{{ form.generation(class_="form-control") }} {{ form.generation(class_="form-control") }}
<small class="text-muted form-text">The generation of the device.</small> <small class="text-muted form-text">The generation of the device.</small>
{% if form.generation.errors %} {% if form.generation.errors %}
@ -225,7 +295,7 @@
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.weight.label }} *</label> <label for="model" class="form-label">{{ form.weight.label }}</label>
{{ form.weight(class_="form-control") }} {{ form.weight(class_="form-control") }}
<small class="text-muted form-text">The weight of the device in Kg.</small> <small class="text-muted form-text">The weight of the device in Kg.</small>
{% if form.weight.errors %} {% if form.weight.errors %}
@ -238,7 +308,7 @@
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.width.label }} *</label> <label for="model" class="form-label">{{ form.width.label }}</label>
{{ form.width(class_="form-control") }} {{ form.width(class_="form-control") }}
<small class="text-muted form-text">The width of the device in meters.</small> <small class="text-muted form-text">The width of the device in meters.</small>
{% if form.width.errors %} {% if form.width.errors %}
@ -251,7 +321,7 @@
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.height.label }} *</label> <label for="model" class="form-label">{{ form.height.label }}</label>
{{ form.height(class_="form-control") }} {{ form.height(class_="form-control") }}
<small class="text-muted form-text">The height of the device in meters.</small> <small class="text-muted form-text">The height of the device in meters.</small>
{% if form.height.errors %} {% if form.height.errors %}
@ -264,7 +334,7 @@
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2">
<label for="model" class="form-label">{{ form.depth.label }} *</label> <label for="model" class="form-label">{{ form.depth.label }}</label>
{{ form.depth(class_="form-control") }} {{ form.depth(class_="form-control") }}
<small class="text-muted form-text">The depth of the device in meters.</small> <small class="text-muted form-text">The depth of the device in meters.</small>
{% if form.depth.errors %} {% if form.depth.errors %}
@ -289,7 +359,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="from-group has-validation mb-2"> <div class="from-group has-validation mb-2" id="Sku">
<label for="model" class="form-label">{{ form.sku.label }}</label> <label for="model" class="form-label">{{ form.sku.label }}</label>
{{ form.sku(class_="form-control") }} {{ form.sku(class_="form-control") }}
<small class="text-muted form-text">The Stock Keeping Unit (SKU), i.e. a merchant-specific identifier for a product or service.</small> <small class="text-muted form-text">The Stock Keeping Unit (SKU), i.e. a merchant-specific identifier for a product or service.</small>

View File

@ -54,7 +54,8 @@
<div class="tab-content pt-2"> <div class="tab-content pt-2">
<div class="tab-pane fade show active" id="type"> <div class="tab-pane fade show active" id="type">
<h5 class="card-title">Type Details</h5> <h5 class="card-title">Details</h5>
{% if device.placeholder %}(<a href="{{ url_for('inventory.device_edit', id=device.devicehub_id)}}">edit</a>){% endif %}
<div class="row"> <div class="row">
<div class="col-lg-3 col-md-4 label ">Type</div> <div class="col-lg-3 col-md-4 label ">Type</div>

View File

@ -315,7 +315,7 @@
<div class="btn-group dropdown m-1" uib-dropdown=""> <div class="btn-group dropdown m-1" uib-dropdown="">
<button id="btnSnapshot" 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-laptop"></i> <i class="bi bi-laptop"></i>
New Device New Devices
</button> </button>
<ul class="dropdown-menu" aria-labelledby="btnSnapshot"> <ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li> <li>
@ -325,7 +325,17 @@
<a href="{{ url_for('inventory.upload_snapshot') }}" class="dropdown-item"> <a href="{{ url_for('inventory.upload_snapshot') }}" class="dropdown-item">
{% endif %} {% endif %}
<i class="bi bi-upload"></i> <i class="bi bi-upload"></i>
Upload a new Snapshot Upload Snapshot files
</a>
</li>
<li>
{% if lot %}
<a href="{{ url_for('inventory.lot_upload_placeholder', lot_id=lot.id) }}" class="dropdown-item">
{% else %}
<a href="{{ url_for('inventory.upload_placeholder') }}" class="dropdown-item">
{% endif %}
<i class="bi bi-upload"></i>
Upload Placeholder Spreadsheet
</a> </a>
</li> </li>
<li> <li>
@ -335,7 +345,7 @@
<a href="{{ url_for('inventory.device_add') }}" class="dropdown-item"> <a href="{{ url_for('inventory.device_add') }}" class="dropdown-item">
{% endif %} {% endif %}
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Create a new Device Create a new Placeholder
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -0,0 +1,80 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ page_title }}</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
<li class="breadcrumb-item active">Placeholders</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-body pt-3" style="min-height: 650px;">
<!-- Bordered Tabs -->
<div class="tab-content pt-5">
<div id="devices-list" class="tab-pane fade devices-list active show">
<div class="tab-content pt-2">
<table class="table">
<thead>
<tr>
<th scope="col">PHID</th>
<th scope="col">Placeholder source</th>
<th scope="col">Type</th>
<th scope="col">DHID</th>
<th scope="col">Status</th>
<th scope="col" data-type="date" data-format="DD-MM-YYYY">Time</th>
</tr>
</thead>
<tbody>
{% for log in placeholders_log %}
<tr>
<td>
{{ log.phid }}
</td>
<td>
{{ log.source }}
</td>
<td>
{{ log.type }}
</td>
<td>
{% if log.dhid %}
<a href="{{ url_for('inventory.device_details', id=log.dhid)}}">{{ log.dhid }}</a>
{% endif %}
</td>
<td>
{{ log.get_status() }}
</td>
<td>{{ log.created.strftime('%H:%M %d-%m-%Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div><!-- End Bordered Tabs -->
</div>
</div>
</div>
<div id="NotificationsContainer" style="position: absolute; bottom: 0; right: 0; margin: 10px; margin-top: 70px; width: calc(100% - 310px);"></div>
</div>
</div>
</section>
<!-- Custom Code -->
<script>
const table = new simpleDatatables.DataTable("table")
</script>
{% endblock main %}

View File

@ -0,0 +1,124 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>Inventory</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('inventory.devicelist')}}">Inventory</a></li>
<li class="breadcrumb-item active">{{ page_title }}</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-8">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">Upload Placeholder</h5>
<p class="text-center small">Please select a Placeholders CSV file.</p>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div class="pt-4 pb-2 mb-3">
Use the following template to register or update placeholders.<br />
Choose the type of device.<br />
The following considerations are important:<br />
1. Do not rename columns or add new columns.<br />
2. Accepted file types are ods, xlsx and csv.<br />
3. A new Placeholder will be registered if the PHID value does not exist in the system or is empty.<br />
4. A Placeholder will be updated if the PHID value exists in the system
</div>
<form method="post" enctype="multipart/form-data" class="row g-3 needs-validation" novalidate>
{{ form.csrf_token }}
<div class="form-group has-validation mb-2">
<label for="name" class="form-label">Type *</label>
<select id="type" class="form-control" name="type" required="">
<option value="">Select one Type</option>
<optgroup label="Computer">
<option value="Laptop"
{% if form.type.data == 'Laptop' %} selected="selected"{% endif %}>Laptop</option>
<option value="Desktop"
{% if form.type.data == 'Desktop' %} selected="selected"{% endif %}>Desktop</option>
<option value="Server"
{% if form.type.data == 'Server' %} selected="selected"{% endif %}>Server</option>
</optgroup>
<optgroup label="Monitor">
<option value="ComputerMonitor"
{% if form.type.data == 'Monitor' %} selected="selected"{% endif %}>Computer Monitor</option>
</optgroup>
<optgroup label="Mobile">
<option value="Smartphone"
{% if form.type.data == 'Smartphone' %} selected="selected"{% endif %}>Smartphone</option>
<option value="Tablet"
{% if form.type.data == 'Tablet' %} selected="selected"{% endif %}>Tablet</option>
<option value="Cellphone"
{% if form.type.data == 'Cellphone' %} selected="selected"{% endif %}>Cellphone</option>
</optgroup>
<optgroup label="Computer Accessory">
<option value="Mouse"
{% if form.type.data == 'Mouse' %} selected="selected"{% endif %}>Mouse</option>
<option value="MemoryCardReader"
{% if form.type.data == 'MemoryCardReader' %} selected="selected"{% endif %}>Memory card reader</option>
<option value="SAI"
{% if form.type.data == 'SAI' %} selected="selected"{% endif %}>SAI</option>
<option value="Keyboard"
{% if form.type.data == 'Keyboard' %} selected="selected"{% endif %}>Keyboard</option>
</optgroup>
</select>
<small class="text-muted form-text">Type of devices</small>
{% if form.type.errors %}
<p class="text-danger">
{% for error in form.type.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div>
<label for="name" class="form-label">Select a Placeholders CSV file *</label>
<div class="input-group has-validation">
{{ form.placeholder_file }}
</div>
{% if form.placeholder_file.errors %}
<p class="text-danger">
{% for error in form.placeholder_file.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
<div>
{% if lot_id %}
<a href="{{ url_for('inventory.lotdevicelist', lot_id=lot_id) }}" class="btn btn-danger">Cancel</a>
{% else %}
<a href="{{ url_for('inventory.devicelist') }}" class="btn btn-danger">Cancel</a>
{% endif %}
<button class="btn btn-primary" type="submit">Send</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
</div>
</div>
</section>
{% endblock main %}

View File

@ -46,5 +46,11 @@ selenium==4.1.5
pyjwt==2.4.0 pyjwt==2.4.0
pint==0.9 pint==0.9
py-dmidecode==0.1.0 py-dmidecode==0.1.0
pandas==1.3.5
numpy==1.21.6
odfpy==1.4.1
xlrd==2.0.1
openpyxl==3.0.10
et_xmlfile==1.1.0
sentry_sdk==1.6.0 sentry_sdk==1.6.0
blinker==1.4 blinker==1.4

View File

@ -0,0 +1,4 @@
Phid;Model;Manufacturer;Serial Number;Id device Supplier;Pallet;Info
a123;Vaio;Sony;12345678;TTT;24A;Good conditions
a124;Vaio;Sony;12345679;TTT;24A;Good conditions
a125;Vaio;Sony;12345680;TTT;24A;Good conditions
1 Phid Model Manufacturer Serial Number Id device Supplier Pallet Info
2 a123 Vaio Sony 12345678 TTT 24A Good conditions
3 a124 Vaio Sony 12345679 TTT 24A Good conditions
4 a125 Vaio Sony 12345680 TTT 24A Good conditions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -445,12 +445,227 @@ def test_add_monitor(user3: UserClientFlask):
'weight': 0.1, 'weight': 0.1,
'height': 0.1, 'height': 0.1,
'depth': 0.1, 'depth': 0.1,
'id_device_supplier': "b2",
} }
body, status = user3.post(uri, data=data) body, status = user3.post(uri, data=data)
assert status == '200 OK' assert status == '200 OK'
assert 'Device &#34;Monitor&#34; created successfully!' in body assert 'Device &#34;Monitor&#34; created successfully!' in body
dev = Device.query.one() dev = Device.query.one()
assert dev.type == 'Monitor' assert dev.type == 'Monitor'
assert dev.placeholder.id_device_supplier == "b2"
assert dev.hid == 'monitor-samsung-lc27t55-aaaab'
assert dev.placeholder.phid == '1'
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_update_monitor(user3: UserClientFlask):
uri = '/inventory/device/add/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "New Device" in body
data = {
'csrf_token': generate_csrf(),
'type': "Monitor",
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "b2",
'pallet': "l34",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Device &#34;Monitor&#34; created successfully!' in body
dev = Device.query.one()
assert dev.type == 'Monitor'
assert dev.placeholder.id_device_supplier == "b2"
assert dev.hid == 'monitor-samsung-lc27t55-aaaab'
assert dev.placeholder.phid == '1'
assert dev.model == 'lc27t55'
assert dev.depth == 0.1
assert dev.placeholder.pallet == "l34"
data = {
'csrf_token': generate_csrf(),
'type': "Monitor",
'phid': '1',
'serial_number': "AAAAB",
'model': "LCD 43 b",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.2,
'id_device_supplier': "b3",
'pallet': "l20",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Sorry, exist one snapshot device with this HID' in body
dev = Device.query.one()
assert dev.type == 'Monitor'
assert dev.placeholder.id_device_supplier == "b2"
assert dev.hid == 'monitor-samsung-lc27t55-aaaab'
assert dev.placeholder.phid == '1'
assert dev.model == 'lc27t55'
assert dev.depth == 0.1
assert dev.placeholder.pallet == "l34"
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_2_monitor(user3: UserClientFlask):
uri = '/inventory/device/add/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "New Device" in body
data = {
'csrf_token': generate_csrf(),
'type': "Monitor",
'phid': "AAB",
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "b1",
'pallet': "l34",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Device &#34;Monitor&#34; created successfully!' in body
dev = Device.query.one()
assert dev.type == 'Monitor'
assert dev.placeholder.id_device_supplier == "b1"
assert dev.hid == 'monitor-samsung-lc27t55-aaaab'
assert dev.placeholder.phid == 'AAB'
assert dev.model == 'lc27t55'
assert dev.placeholder.pallet == "l34"
data = {
'csrf_token': generate_csrf(),
'type': "Monitor",
'serial_number': "AAAAB",
'model': "LCD 43 b",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.2,
'id_device_supplier': "b2",
'pallet': "l20",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Device &#34;Monitor&#34; created successfully!' in body
dev = Device.query.all()[-1]
assert dev.type == 'Monitor'
assert dev.placeholder.id_device_supplier == "b2"
assert dev.hid == 'monitor-samsung-lcd_43_b-aaaab'
assert dev.placeholder.phid == '2'
assert dev.model == 'lcd 43 b'
assert dev.placeholder.pallet == "l20"
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_laptop(user3: UserClientFlask):
uri = '/inventory/device/add/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "New Device" in body
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "b2",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Device &#34;Laptop&#34; created successfully!' in body
dev = Device.query.one()
assert dev.type == 'Laptop'
assert dev.placeholder.id_device_supplier == "b2"
assert dev.hid == 'laptop-samsung-lc27t55-aaaab'
assert dev.placeholder.phid == '1'
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_with_ammount_laptops(user3: UserClientFlask):
uri = '/inventory/device/add/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "New Device" in body
num = 3
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'amount': num,
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "b2",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Device &#34;Laptop&#34; created successfully!' in body
for dev in Device.query.all():
assert dev.type == 'Laptop'
assert dev.placeholder.id_device_supplier is None
assert dev.hid is None
assert dev.placeholder.phid in [str(x) for x in range(1, num + 1)]
assert Device.query.count() == num
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_laptop_duplicate(user3: UserClientFlask):
uri = '/inventory/device/add/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "New Device" in body
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'phid': 'laptop-asustek_computer_inc-1001pxd-b8oaas048285-14:da:e9:42:f6:7b',
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert Device.query.count() == 1
body, status = user3.post(uri, data=data)
assert 'Sorry, exist one snapshot device with this HID' in body
assert Device.query.count() == 1
@pytest.mark.mvp @pytest.mark.mvp
@ -1400,3 +1615,326 @@ def test_export_snapshot_json(user3: UserClientFlask):
body, status = user3.get(uri) body, status = user3.get(uri)
assert status == '200 OK' assert status == '200 OK'
assert body == snapshot assert body == snapshot
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_placeholder_excel(user3: UserClientFlask):
uri = '/inventory/upload-placeholder/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Upload Placeholder" in body
file_path = Path(__file__).parent.joinpath('files').joinpath('placeholder_test.xls')
with open(file_path, 'rb') as excel:
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'placeholder_file': excel,
}
user3.post(uri, data=data, content_type="multipart/form-data")
assert Device.query.count() == 3
dev = Device.query.first()
assert dev.hid == 'laptop-sony-vaio-12345678'
assert dev.placeholder.phid == 'a123'
assert dev.placeholder.info == 'Good conditions'
assert dev.placeholder.pallet == '24A'
assert dev.placeholder.id_device_supplier == 'TTT'
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_placeholder_csv(user3: UserClientFlask):
uri = '/inventory/upload-placeholder/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Upload Placeholder" in body
file_path = Path(__file__).parent.joinpath('files').joinpath('placeholder_test.csv')
with open(file_path, 'rb') as excel:
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'placeholder_file': excel,
}
user3.post(uri, data=data, content_type="multipart/form-data")
assert Device.query.count() == 3
dev = Device.query.first()
assert dev.hid == 'laptop-sony-vaio-12345678'
assert dev.placeholder.phid == 'a123'
assert dev.placeholder.info == 'Good conditions'
assert dev.placeholder.pallet == '24A'
assert dev.placeholder.id_device_supplier == 'TTT'
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_placeholder_ods(user3: UserClientFlask):
uri = '/inventory/upload-placeholder/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Upload Placeholder" in body
file_path = Path(__file__).parent.joinpath('files').joinpath('placeholder_test.ods')
with open(file_path, 'rb') as excel:
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'placeholder_file': excel,
}
user3.post(uri, data=data, content_type="multipart/form-data")
assert Device.query.count() == 3
dev = Device.query.first()
assert dev.hid == 'laptop-sony-vaio-12345678'
assert dev.placeholder.phid == 'a123'
assert dev.placeholder.info == 'Good conditions'
assert dev.placeholder.pallet == '24A'
assert dev.placeholder.id_device_supplier == 'TTT'
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_placeholder_office_open_xml(user3: UserClientFlask):
uri = '/inventory/upload-placeholder/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Upload Placeholder" in body
file_path = (
Path(__file__).parent.joinpath('files').joinpath('placeholder_test.xlsx')
)
with open(file_path, 'rb') as excel:
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'placeholder_file': excel,
}
user3.post(uri, data=data, content_type="multipart/form-data")
assert Device.query.count() == 3
dev = Device.query.first()
assert dev.hid == 'laptop-sony-vaio-12345678'
assert dev.placeholder.phid == 'a123'
assert dev.placeholder.info == 'Good conditions'
assert dev.placeholder.pallet == '24A'
assert dev.placeholder.id_device_supplier == 'TTT'
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_edit_laptop(user3: UserClientFlask):
uri = '/inventory/device/add/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "New Device" in body
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "b2",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Device &#34;Laptop&#34; created successfully!' in body
dev = Device.query.one()
assert dev.type == 'Laptop'
assert dev.hid == 'laptop-samsung-lc27t55-aaaab'
assert dev.placeholder.phid == '1'
assert dev.placeholder.id_device_supplier == 'b2'
assert dev.serial_number == 'aaaab'
assert dev.model == 'lc27t55'
uri = '/inventory/device/edit/{}/'.format(dev.devicehub_id)
body, status = user3.get(uri)
assert status == '200 OK'
assert "Edit Device" in body
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'serial_number': "AAAAC",
'model': "LC27T56",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "a2",
}
body, status = user3.post(uri, data=data)
assert status == '200 OK'
assert 'Device &#34;Laptop&#34; edited successfully!' in body
dev = Device.query.one()
assert dev.type == 'Laptop'
assert dev.hid == 'laptop-samsung-lc27t55-aaaab'
assert dev.placeholder.phid == '1'
assert dev.placeholder.id_device_supplier == 'a2'
assert dev.serial_number == 'aaaac'
assert dev.model == 'lc27t56'
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_placeholder_log_manual_new(user3: UserClientFlask):
uri = '/inventory/device/add/'
user3.get(uri)
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'phid': 'ace',
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "b2",
}
user3.post(uri, data=data)
uri = '/inventory/placeholder-logs/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Placeholder Logs" in body
assert "Web form" in body
assert "ace" in body
assert "New device" in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_placeholder_log_manual_edit(user3: UserClientFlask):
uri = '/inventory/device/add/'
user3.get(uri)
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'phid': 'ace',
'serial_number': "AAAAB",
'model': "LC27T55",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "b2",
}
user3.post(uri, data=data)
dev = Device.query.one()
uri = '/inventory/device/edit/{}/'.format(dev.devicehub_id)
user3.get(uri)
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'serial_number': "AAAAC",
'model': "LC27T56",
'manufacturer': "Samsung",
'generation': 1,
'weight': 0.1,
'height': 0.1,
'depth': 0.1,
'id_device_supplier': "a2",
}
user3.post(uri, data=data)
uri = '/inventory/placeholder-logs/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Placeholder Logs" in body
assert "Web form" in body
assert "ace" in body
assert "Update" in body
assert dev.devicehub_id in body
assert "" in body
assert "CSV" not in body
assert "Excel" not in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_placeholder_log_excel_new(user3: UserClientFlask):
uri = '/inventory/upload-placeholder/'
body, status = user3.get(uri)
file_path = Path(__file__).parent.joinpath('files').joinpath('placeholder_test.xls')
with open(file_path, 'rb') as excel:
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'placeholder_file': excel,
}
user3.post(uri, data=data, content_type="multipart/form-data")
dev = Device.query.first()
assert dev.placeholder.phid == 'a123'
uri = '/inventory/placeholder-logs/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Placeholder Logs" in body
assert dev.placeholder.phid in body
assert dev.devicehub_id in body
assert "Web form" not in body
assert "Update" not in body
assert "New device" in body
assert "" in body
assert "CSV" not in body
assert "Excel" in body
assert "placeholder_test.xls" in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_placeholder_log_excel_update(user3: UserClientFlask):
uri = '/inventory/upload-placeholder/'
body, status = user3.get(uri)
file_path = Path(__file__).parent.joinpath('files').joinpath('placeholder_test.xls')
with open(file_path, 'rb') as excel:
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'placeholder_file': excel,
}
user3.post(uri, data=data, content_type="multipart/form-data")
file_path = Path(__file__).parent.joinpath('files').joinpath('placeholder_test.csv')
with open(file_path, 'rb') as excel:
data = {
'csrf_token': generate_csrf(),
'type': "Laptop",
'placeholder_file': excel,
}
user3.post(uri, data=data, content_type="multipart/form-data")
dev = Device.query.first()
assert dev.placeholder.phid == 'a123'
uri = '/inventory/placeholder-logs/'
body, status = user3.get(uri)
assert status == '200 OK'
assert "Placeholder Logs" in body
assert dev.placeholder.phid in body
assert dev.devicehub_id in body
assert "Web form" not in body
assert "Update" in body
assert "New device" in body
assert "" in body
assert "CSV" in body
assert "Excel" in body
assert "placeholder_test.xls" in body
assert "placeholder_test.csv" in body