diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index 0e9431d3..cd91deb8 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -455,7 +455,7 @@ class NewDeviceForm(FlaskForm): 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 + Placeholder.phid == self.phid.data, Placeholder.owner == g.user ).first() if dev: msg = "Sorry, exist one snapshot device with this HID" @@ -568,6 +568,7 @@ class NewDeviceForm(FlaskForm): 'id_device_supplier': self.id_device_supplier.data, 'info': self.info.data, 'pallet': self.pallet.data, + 'is_abstract': False, } ) return self.placeholder @@ -577,6 +578,7 @@ class NewDeviceForm(FlaskForm): 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.placeholder.is_abstract = False self._obj.model = self.model.data self._obj.manufacturer = self.manufacturer.data self._obj.serial_number = self.serial_number.data @@ -1555,6 +1557,7 @@ class UploadPlaceholderForm(FlaskForm): 'id_device_supplier': data['Id device Supplier'][i], 'pallet': data['Pallet'][i], 'info': data['Info'][i], + 'is_abstract': False, } snapshot_json = schema.load(json_snapshot) @@ -1606,3 +1609,42 @@ class EditPlaceholderForm(FlaskForm): db.session.commit() return self.placeholders + + +class BindingForm(FlaskForm): + phid = StringField('Phid', [validators.DataRequired()]) + + def __init__(self, *args, **kwargs): + self.device = kwargs.pop('device', None) + self.placeholder = kwargs.pop('placeholder', None) + super().__init__(*args, **kwargs) + + def validate(self, extra_validators=None): + is_valid = super().validate(extra_validators) + + if not is_valid: + txt = "This placeholder not exist." + self.phid.errors = [txt] + return False + + if self.device.placeholder: + txt = "This is not a device Workbench." + self.phid.errors = [txt] + return False + + if not self.placeholder: + self.placeholder = Placeholder.query.filter( + Placeholder.phid == self.phid.data, Placeholder.owner == g.user + ).first() + + if not self.placeholder: + txt = "This placeholder not exist." + self.phid.errors = [txt] + return False + + if self.placeholder.binding: + txt = "This placeholder have a binding with other device. Before you need to do an unbinding with this other device." + self.phid.errors = [txt] + return False + + return True diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index e8ccf411..63cd6dc6 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -19,6 +19,7 @@ from ereuse_devicehub.db import db from ereuse_devicehub.inventory.forms import ( AdvancedSearchForm, AllocateForm, + BindingForm, DataWipeForm, EditTransferForm, FilterForm, @@ -36,7 +37,12 @@ from ereuse_devicehub.inventory.forms import ( from ereuse_devicehub.labels.forms import PrintLabelsForm 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.device.models import ( + Computer, + DataStorage, + Device, + Placeholder, +) from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.enums import SnapshotSoftware from ereuse_devicehub.resources.hash_reports import insert_hash @@ -129,6 +135,7 @@ class AdvancedSearchView(DeviceListMixin): class DeviceDetailView(GenericMixin): + methods = ['GET', 'POST'] decorators = [login_required] template_name = 'inventory/device_detail.html' @@ -140,13 +147,73 @@ class DeviceDetailView(GenericMixin): .one() ) + form_binding = BindingForm(device=device) + self.context.update( { 'device': device, 'placeholder': device.binding or device.placeholder, 'page_title': 'Device {}'.format(device.devicehub_id), + 'form_binding': form_binding, + 'active_binding': False, } ) + + if form_binding.validate_on_submit(): + next_url = url_for( + 'inventory.binding', + dhid=form_binding.device.devicehub_id, + phid=form_binding.placeholder.phid, + ) + return flask.redirect(next_url) + elif form_binding.phid.data: + self.context['active_binding'] = True + + return flask.render_template(self.template_name, **self.context) + + +class BindingView(GenericMixin): + methods = ['GET', 'POST'] + decorators = [login_required] + template_name = 'inventory/binding.html' + + def dispatch_request(self, dhid, phid): + self.get_context() + device = ( + Device.query.filter(Device.owner_id == g.user.id) + .filter(Device.devicehub_id == dhid) + .one() + ) + placeholder = ( + Placeholder.query.filter(Placeholder.owner_id == g.user.id) + .filter(Placeholder.phid == phid) + .one() + ) + + if request.method == 'POST': + old_placeholder = device.binding + if old_placeholder.is_abstract: + for plog in PlaceholdersLog.query.filter_by( + placeholder_id=old_placeholder.id + ): + db.session.delete(plog) + db.session.delete(old_placeholder) + device.binding = placeholder + db.session.commit() + next_url = url_for('inventory.device_details', id=dhid) + messages.success( + 'Device "{}" bind successfully with {}!'.format(dhid, phid) + ) + return flask.redirect(next_url) + + self.context.update( + { + 'device': device.binding.device, + 'placeholder': placeholder, + 'page_title': 'Binding confirm', + } + ) + return flask.render_template(self.template_name, **self.context) @@ -994,3 +1061,6 @@ devices.add_url_rule( devices.add_url_rule( '/placeholder-logs/', view_func=PlaceholderLogListView.as_view('placeholder_logs') ) +devices.add_url_rule( + '/binding///', view_func=BindingView.as_view('binding') +) diff --git a/ereuse_devicehub/migrations/versions/d7ea9a3b2da1_add_owner_to_placeholder.py b/ereuse_devicehub/migrations/versions/d7ea9a3b2da1_add_owner_to_placeholder.py new file mode 100644 index 00000000..fca1d0c0 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/d7ea9a3b2da1_add_owner_to_placeholder.py @@ -0,0 +1,71 @@ +"""add owner to placeholder + +Revision ID: d7ea9a3b2da1 +Revises: 2b90b41a556a +Create Date: 2022-07-27 14:40:15.513820 + +""" +import sqlalchemy as sa +from alembic import context, op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'd7ea9a3b2da1' +down_revision = '2b90b41a556a' +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_data(): + con = op.get_bind() + sql = f"select {get_inv()}.placeholder.id, {get_inv()}.device.owner_id from {get_inv()}.placeholder" + sql += f" join {get_inv()}.device on {get_inv()}.device.id={get_inv()}.placeholder.device_id;" + + for c in con.execute(sql): + id_placeholder = c.id + id_owner = c.owner_id + sql_update = f"update {get_inv()}.placeholder set owner_id='{id_owner}', is_abstract=False where id={id_placeholder};" + con.execute(sql_update) + + +def upgrade(): + op.add_column( + 'placeholder', + sa.Column('is_abstract', sa.Boolean(), nullable=True), + schema=f'{get_inv()}', + ) + op.add_column( + 'placeholder', + sa.Column('owner_id', postgresql.UUID(), nullable=True), + schema=f'{get_inv()}', + ) + op.create_foreign_key( + "fk_placeholder_owner_id_user_id", + "placeholder", + "user", + ["owner_id"], + ["id"], + ondelete="SET NULL", + source_schema=f'{get_inv()}', + referent_schema='common', + ) + + upgrade_data() + + +def downgrade(): + op.drop_constraint( + "fk_placeholder_owner_id_user_id", + "placeholder", + type_="foreignkey", + schema=f'{get_inv()}', + ) + op.drop_column('placeholder', 'owner_id', schema=f'{get_inv()}') + op.drop_column('placeholder', 'is_abstract', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index ee0d08b5..03cceeea 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -852,6 +852,7 @@ class Placeholder(Thing): pallet.comment = "used for identification where from where is this placeholders" info = db.Column(CIText()) info.comment = "more info of placeholders" + is_abstract = db.Column(Boolean, default=False) id_device_supplier = db.Column(CIText()) id_device_supplier.comment = ( "Identification used for one supplier of one placeholders" @@ -880,6 +881,13 @@ class Placeholder(Thing): primaryjoin=binding_id == Device.id, ) binding_id.comment = "binding placeholder with workbench device" + 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) class Computer(Device): diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index 46850303..3f284e71 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -310,11 +310,15 @@ class Sync: c_placeholder = c.__class__(**c_dict) c_placeholder.parent = dev_placeholder c.parent = device - component_placeholder = Placeholder(device=c_placeholder, binding=c) + component_placeholder = Placeholder( + device=c_placeholder, binding=c, is_abstract=True + ) db.session.add(c_placeholder) db.session.add(component_placeholder) - placeholder = Placeholder(device=dev_placeholder, binding=device) + placeholder = Placeholder( + device=dev_placeholder, binding=device, is_abstract=True + ) db.session.add(dev_placeholder) db.session.add(placeholder) diff --git a/ereuse_devicehub/templates/inventory/binding.html b/ereuse_devicehub/templates/inventory/binding.html new file mode 100644 index 00000000..e26fb1bb --- /dev/null +++ b/ereuse_devicehub/templates/inventory/binding.html @@ -0,0 +1,185 @@ +{% extends "ereuse_devicehub/base_site.html" %} +{% block main %} + +
+

{{ title }}

+ +
+ +
+
+
+ +
+
+ +
+
{{ title }}
+

Please check that the information is correct.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Basic DataInfo to be EnteredInfo to be Decoupled
PHID:{{ placeholder.phid or '' }}{{ device.placeholder.phid or '' }}
Manufacturer:{{ placeholder.device.manufacturer or '' }}{{ device.manufacturer or '' }}
Model:{{ placeholder.device.model or '' }}{{ device.model or '' }}
Serial Number:{{ placeholder.device.serial_number or '' }}{{ device.serial_number or '' }}
Brand:{{ placeholder.device.brand or '' }}{{ device.brand or '' }}
Sku:{{ placeholder.device.sku or '' }}{{ device.sku or '' }}
Generation:{{ placeholder.device.generation or '' }}{{ device.generation or '' }}
Version:{{ placeholder.device.version or '' }}{{ device.version or '' }}
Weight:{{ placeholder.device.weight or '' }}{{ device.weight or '' }}
Width:{{ placeholder.device.width or '' }}{{ device.width or '' }}
Height:{{ placeholder.device.height or '' }}{{ device.height or '' }}
Depth:{{ placeholder.device.depth or '' }}{{ device.depth or '' }}
Color:{{ placeholder.device.color or '' }}{{ device.color or '' }}
Production date:{{ placeholder.device.production_date or '' }}{{ device.production_date or '' }}
Variant:{{ placeholder.device.variant or '' }}{{ device.variant or '' }}
+ +
+ + {% if placeholder.device.components or device.components %} +

Components

+ + + + + + + + + + + + + +
Info to be EnteredInfo to be Decoupled
+ {% for c in placeholder.device.components %} + * {{ c.verbose_name }}
+ {% endfor %} +
+ {% for c in device.components %} + * {{ c.verbose_name }}
+ {% endfor %} +
+ {% endif %} + +
+ + {% if placeholder.device.actions or device.actions %} +

Actions

+ + + + + + + + + + + + + +
Info to be EnteredInfo to be Decoupled
+ {% for a in placeholder.device.actions %} + * {{ a.t }}
+ {% endfor %} +
+ {% for a in device.actions %} + * {{ a.t }}
+ {% endfor %} +
+ {% endif %} + +
+
+ Cancel + +
+
+ +
+ +
+ +
+ +
+
+
+
+{% endblock main %} diff --git a/ereuse_devicehub/templates/inventory/device_detail.html b/ereuse_devicehub/templates/inventory/device_detail.html index ea6656e3..2274e676 100644 --- a/ereuse_devicehub/templates/inventory/device_detail.html +++ b/ereuse_devicehub/templates/inventory/device_detail.html @@ -62,10 +62,16 @@ + {% if device.binding %} + + {% endif %} +
-
+
Details
{% if device.placeholder %}(edit){% endif %} @@ -216,6 +222,42 @@ {% endfor %}
+ {% if placeholder.binding %} +
+
Binding
+
+

+ Be careful, binding implies changes in the data of a device that affect its + traceability. +

+
+
+
+ {{ form_binding.csrf_token }} + {% for field in form_binding %} + {% if field != form_binding.csrf_token %} + +
+ {{ field.label(class_="form-label") }}: + {{ field }} + {% if field.errors %} +

+ {% for error in field.errors %} + {{ error }}
+ {% endfor %} +

+ {% endif %} +
+ + {% endif %} + {% endfor %} +
+ +
+
+
+
+ {% endif %}
diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index a511d397..cd8f35a2 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -401,6 +401,8 @@ Select Title DHID + PHID + Is Abstract Unique Identifiers Lifecycle Status Allocated Status @@ -442,6 +444,12 @@ {{ dev.devicehub_id }} + + {{ dev.binding and dev.binding.phid or dev.placeholder and dev.placeholder.phid or '' }} + + + {{ dev.binding and dev.binding.is_abstract and '✓' or dev.placeholder and dev.placeholder.is_abstract and '✓' or '' }} + {% for t in dev.tags | sort(attribute="id") %} {{ t.id }}