diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fb8c4e..218a97ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ml). ## testing +- [added] #312 Placeholder: new, edit, update. (manually and with excel). ## [2.3.0] - 2022-07-12 - [added] #281 Add selenium test. diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index f338e102..65d2b5c3 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -3,6 +3,7 @@ import datetime import json from json.decoder import JSONDecodeError +import pandas as pd from boltons.urlutils import URL from flask import current_app as app from flask import g, request @@ -29,6 +30,7 @@ from wtforms.fields import FormField from ereuse_devicehub.db import db 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.schemas import Snapshot_lite from ereuse_devicehub.resources.action.models import Snapshot, Trade @@ -42,14 +44,17 @@ from ereuse_devicehub.resources.device.models import ( SAI, Cellphone, ComputerMonitor, + Desktop, Device, Keyboard, + Laptop, MemoryCardReader, Mouse, + Placeholder, + Server, Smartphone, Tablet, ) -from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.documents.models import DataWipeDocument from ereuse_devicehub.resources.enums import Severity from ereuse_devicehub.resources.hash_reports import insert_hash @@ -300,19 +305,27 @@ class UploadSnapshotForm(SnapshotMixin, FlaskForm): class NewDeviceForm(FlaskForm): type = StringField('Type', [validators.DataRequired()]) - label = StringField('Label') - serial_number = StringField('Seria Number', [validators.DataRequired()]) - model = StringField('Model', [validators.DataRequired()]) - manufacturer = StringField('Manufacturer', [validators.DataRequired()]) + amount = IntegerField( + 'Amount', + [validators.DataRequired(), validators.NumberRange(min=1, max=100)], + 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()]) functionality = StringField('Functionality', [validators.Optional()]) brand = StringField('Brand') generation = IntegerField('Generation') version = StringField('Version') - weight = FloatField('Weight', [validators.DataRequired()]) - width = FloatField('Width', [validators.DataRequired()]) - height = FloatField('Height', [validators.DataRequired()]) - depth = FloatField('Depth', [validators.DataRequired()]) + weight = FloatField('Weight', [validators.Optional()]) + width = FloatField('Width', [validators.Optional()]) + height = FloatField('Height', [validators.Optional()]) + depth = FloatField('Depth', [validators.Optional()]) variant = StringField('Variant', [validators.Optional()]) sku = StringField('SKU', [validators.Optional()]) image = StringField('Image', [validators.Optional(), validators.URL()]) @@ -322,8 +335,16 @@ class NewDeviceForm(FlaskForm): screen = FloatField('Screen size', [validators.Optional()]) def __init__(self, *args, **kwargs): + self._obj = kwargs.pop('_obj', None) super().__init__(*args, **kwargs) + if self._obj: + self.type.data = self._obj.type + if not request.form: + self.reset_from_obj() self.devices = { + "Laptop": Laptop, + "Desktop": Desktop, + "Server": Server, "Smartphone": Smartphone, "Tablet": Tablet, "Cellphone": Cellphone, @@ -349,6 +370,45 @@ class NewDeviceForm(FlaskForm): if not self.depth.data: 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 error = ["Not a correct value"] is_valid = super().validate(extra_validators) @@ -373,12 +433,12 @@ class NewDeviceForm(FlaskForm): self.depth.errors = error 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: self.imei.errors = error is_valid = False - if self.meid.data: + if self.meid.data and self.amount.data == 1: meid = self.meid.data if not 13 < len(meid) < 17: is_valid = False @@ -388,6 +448,28 @@ class NewDeviceForm(FlaskForm): self.meid.errors = error 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: return False @@ -403,7 +485,18 @@ class NewDeviceForm(FlaskForm): return 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 = { 'type': 'Snapshot', 'software': 'Web', @@ -434,33 +527,84 @@ class NewDeviceForm(FlaskForm): '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) + device = snapshot_json['device'] if self.type.data == 'ComputerMonitor': - snapshot_json['device'].resolution_width = self.resolution.data - snapshot_json['device'].size = self.screen.data + device.resolution_width = self.resolution.data + device.size = self.screen.data if self.type.data in ['Smartphone', 'Tablet', 'Cellphone']: - snapshot_json['device'].imei = self.imei.data - snapshot_json['device'].meid = self.meid.data + device.imei = self.imei.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) - if self.type.data == 'ComputerMonitor': - snapshot.device.resolution = self.resolution.data - snapshot.device.screen = self.screen.data + placeholder_log = PlaceholdersLog( + type="New device", source='Web form', placeholder=device.placeholder + ) + db.session.add(placeholder_log) - if commit: - db.session.commit() - return snapshot + def reset_ids(self): + if self.amount.data > 1: + 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): @@ -1295,3 +1439,164 @@ class NotesForm(FlaskForm): db.session.commit() 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 diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index da4df610..52a1a04a 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -30,10 +30,11 @@ from ereuse_devicehub.inventory.forms import ( TradeDocumentForm, TradeForm, TransferForm, + UploadPlaceholderForm, UploadSnapshotForm, ) 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.device.models import Computer, DataStorage, Device 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) +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): methods = ['POST'] decorators = [login_required] - # template_name = 'inventory/device_list.html' def dispatch_request(self): form = TagDeviceForm() @@ -832,6 +860,53 @@ class ReceiverNoteView(GenericMixin): 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/trade/add/', view_func=NewTradeView.as_view('trade_add')) devices.add_url_rule( @@ -871,6 +946,9 @@ devices.add_url_rule( '/lot//device/add/', view_func=DeviceCreateView.as_view('lot_device_add'), ) +devices.add_url_rule( + '/device/edit//', view_func=DeviceEditView.as_view('device_edit') +) devices.add_url_rule( '/tag/devices/add/', view_func=TagLinkDeviceView.as_view('tag_devices_add') ) @@ -902,3 +980,14 @@ devices.add_url_rule( '/lot//receivernote/', 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//upload-placeholder/', + view_func=UploadPlaceholderView.as_view('lot_upload_placeholder'), +) +devices.add_url_rule( + '/placeholder-logs/', view_func=PlaceholderLogListView.as_view('placeholder_logs') +) diff --git a/ereuse_devicehub/migrations/versions/3e3a67f62972_placeholder_log.py b/ereuse_devicehub/migrations/versions/3e3a67f62972_placeholder_log.py new file mode 100644 index 00000000..f51b3749 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/3e3a67f62972_placeholder_log.py @@ -0,0 +1,66 @@ +"""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'], + [f'{get_inv()}.placeholder.id'], + ), + sa.ForeignKeyConstraint( + ['owner_id'], + ['common.user.id'], + ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.execute("CREATE SEQUENCE placeholders_log_seq START 1;") + + +def downgrade(): + op.drop_table('placeholders_log', schema=f'{get_inv()}') + op.execute("DROP SEQUENCE placeholders_log_seq;") diff --git a/ereuse_devicehub/migrations/versions/aeca9fb50cc6_add_placeholder.py b/ereuse_devicehub/migrations/versions/aeca9fb50cc6_add_placeholder.py new file mode 100644 index 00000000..84137bab --- /dev/null +++ b/ereuse_devicehub/migrations/versions/aeca9fb50cc6_add_placeholder.py @@ -0,0 +1,60 @@ +"""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.Column('binding_id', sa.BigInteger(), nullable=True), + sa.ForeignKeyConstraint(['device_id'], [f'{get_inv()}.device.id']), + sa.ForeignKeyConstraint(['binding_id'], [f'{get_inv()}.device.id']), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}', + ) + op.execute("CREATE SEQUENCE placeholder_seq START 1;") + + +def downgrade(): + op.drop_table('placeholder', schema=f'{get_inv()}') + op.execute("DROP SEQUENCE placeholder_seq;") diff --git a/ereuse_devicehub/parser/models.py b/ereuse_devicehub/parser/models.py index 2da2f3ca..69cd574c 100644 --- a/ereuse_devicehub/parser/models.py +++ b/ereuse_devicehub/parser/models.py @@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import UUID from ereuse_devicehub.db import db 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.models import Thing from ereuse_devicehub.resources.user.models import User @@ -43,3 +44,47 @@ class SnapshotsLog(Thing): return self.snapshot.device.devicehub_id return '' + + +class PlaceholdersLog(Thing): + """A Placeholder log.""" + + 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) diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 5fd50a1f..0703359d 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -75,6 +75,13 @@ def create_code(context): 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): """Base class for any type of physical object that can be identified. @@ -594,6 +601,34 @@ class Device(Thing): args[POLYMORPHIC_ON] = cls.type 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): from ereuse_devicehub.resources.device import states @@ -791,6 +826,43 @@ class DisplayMixin: return v +class Placeholder(Thing): + 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" + + binding_id = db.Column( + BigInteger, + db.ForeignKey(Device.id), + nullable=True, + ) + binding = db.relationship( + Device, + backref=backref('binding', lazy=True, uselist=False), + primaryjoin=binding_id == Device.id, + ) + binding_id.comment = "binding placeholder with workbench device" + + class Computer(Device): """A chassis with components inside that can be processed automatically with Workbench Computer. diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index 952e935b..dedaf499 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -18,6 +18,7 @@ from ereuse_devicehub.resources.device.models import ( Computer, DataStorage, Device, + Placeholder, ) from ereuse_devicehub.resources.tag.model import Tag @@ -195,6 +196,7 @@ class Sync: db_device = Device.query.filter_by( hid=device.hid, owner_id=g.user.id, active=True ).one() + if db_device and db_device.allocated: raise ResourceNotFound('device is actually allocated {}'.format(device)) @@ -226,6 +228,7 @@ class Sync: device.physical_properties, ) db_device = sample_tag.device + if db_device: # Device from hid or tags self.merge(device, db_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: return + if device.placeholder and not db_device.placeholder: + return + for field_name, value in device.physical_properties.items(): if value is not None: setattr(db_device, field_name, value) @@ -272,6 +278,23 @@ class Sync: if hasattr(device, 'system_uuid') and 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 def add_remove(device: Computer, components: Set[Component]) -> OrderedSet: """Generates the Add and Remove actions (but doesn't add them to diff --git a/ereuse_devicehub/static/js/create_device.js b/ereuse_devicehub/static/js/create_device.js index 53d77dfa..04533411 100644 --- a/ereuse_devicehub/static/js/create_device.js +++ b/ereuse_devicehub/static/js/create_device.js @@ -1,6 +1,8 @@ $(document).ready(() => { $("#type").on("change", deviceInputs); + $("#amount").on("change", amountInputs); deviceInputs(); + amountInputs(); }) function deviceInputs() { @@ -19,5 +21,23 @@ function deviceInputs() { $("#resolution").hide(); $("#imei").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(); + }; } diff --git a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html index 2eeb716a..96c9cf47 100644 --- a/ereuse_devicehub/templates/ereuse_devicehub/base_site.html +++ b/ereuse_devicehub/templates/ereuse_devicehub/base_site.html @@ -148,6 +148,15 @@ + + + +