add manual binding

This commit is contained in:
Cayo Puigdefabregas 2022-07-28 17:48:14 +02:00
parent d008bb2921
commit 497e29a2da
8 changed files with 435 additions and 5 deletions

View File

@ -455,7 +455,7 @@ class NewDeviceForm(FlaskForm):
if self.phid.data and self.amount.data == 1 and not self._obj: if self.phid.data and self.amount.data == 1 and not self._obj:
dev = Placeholder.query.filter( dev = Placeholder.query.filter(
Placeholder.phid == self.phid.data, Device.owner == g.user Placeholder.phid == self.phid.data, Placeholder.owner == g.user
).first() ).first()
if dev: if dev:
msg = "Sorry, exist one snapshot device with this HID" msg = "Sorry, exist one snapshot device with this HID"
@ -568,6 +568,7 @@ class NewDeviceForm(FlaskForm):
'id_device_supplier': self.id_device_supplier.data, 'id_device_supplier': self.id_device_supplier.data,
'info': self.info.data, 'info': self.info.data,
'pallet': self.pallet.data, 'pallet': self.pallet.data,
'is_abstract': False,
} }
) )
return self.placeholder 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.id_device_supplier = self.id_device_supplier.data or None
self._obj.placeholder.info = self.info.data or None self._obj.placeholder.info = self.info.data or None
self._obj.placeholder.pallet = self.pallet.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.model = self.model.data
self._obj.manufacturer = self.manufacturer.data self._obj.manufacturer = self.manufacturer.data
self._obj.serial_number = self.serial_number.data self._obj.serial_number = self.serial_number.data
@ -1555,6 +1557,7 @@ class UploadPlaceholderForm(FlaskForm):
'id_device_supplier': data['Id device Supplier'][i], 'id_device_supplier': data['Id device Supplier'][i],
'pallet': data['Pallet'][i], 'pallet': data['Pallet'][i],
'info': data['Info'][i], 'info': data['Info'][i],
'is_abstract': False,
} }
snapshot_json = schema.load(json_snapshot) snapshot_json = schema.load(json_snapshot)
@ -1606,3 +1609,42 @@ class EditPlaceholderForm(FlaskForm):
db.session.commit() db.session.commit()
return self.placeholders 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

View File

@ -19,6 +19,7 @@ from ereuse_devicehub.db import db
from ereuse_devicehub.inventory.forms import ( from ereuse_devicehub.inventory.forms import (
AdvancedSearchForm, AdvancedSearchForm,
AllocateForm, AllocateForm,
BindingForm,
DataWipeForm, DataWipeForm,
EditTransferForm, EditTransferForm,
FilterForm, FilterForm,
@ -36,7 +37,12 @@ from ereuse_devicehub.inventory.forms import (
from ereuse_devicehub.labels.forms import PrintLabelsForm from ereuse_devicehub.labels.forms import PrintLabelsForm
from ereuse_devicehub.parser.models import PlaceholdersLog, 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,
Placeholder,
)
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
from ereuse_devicehub.resources.enums import SnapshotSoftware from ereuse_devicehub.resources.enums import SnapshotSoftware
from ereuse_devicehub.resources.hash_reports import insert_hash from ereuse_devicehub.resources.hash_reports import insert_hash
@ -129,6 +135,7 @@ class AdvancedSearchView(DeviceListMixin):
class DeviceDetailView(GenericMixin): class DeviceDetailView(GenericMixin):
methods = ['GET', 'POST']
decorators = [login_required] decorators = [login_required]
template_name = 'inventory/device_detail.html' template_name = 'inventory/device_detail.html'
@ -140,13 +147,73 @@ class DeviceDetailView(GenericMixin):
.one() .one()
) )
form_binding = BindingForm(device=device)
self.context.update( self.context.update(
{ {
'device': device, 'device': device,
'placeholder': device.binding or device.placeholder, 'placeholder': device.binding or device.placeholder,
'page_title': 'Device {}'.format(device.devicehub_id), '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) return flask.render_template(self.template_name, **self.context)
@ -994,3 +1061,6 @@ devices.add_url_rule(
devices.add_url_rule( devices.add_url_rule(
'/placeholder-logs/', view_func=PlaceholderLogListView.as_view('placeholder_logs') '/placeholder-logs/', view_func=PlaceholderLogListView.as_view('placeholder_logs')
) )
devices.add_url_rule(
'/binding/<string:dhid>/<string:phid>/', view_func=BindingView.as_view('binding')
)

View File

@ -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()}')

View File

@ -852,6 +852,7 @@ class Placeholder(Thing):
pallet.comment = "used for identification where from where is this placeholders" pallet.comment = "used for identification where from where is this placeholders"
info = db.Column(CIText()) info = db.Column(CIText())
info.comment = "more info of placeholders" info.comment = "more info of placeholders"
is_abstract = db.Column(Boolean, default=False)
id_device_supplier = db.Column(CIText()) id_device_supplier = db.Column(CIText())
id_device_supplier.comment = ( id_device_supplier.comment = (
"Identification used for one supplier of one placeholders" "Identification used for one supplier of one placeholders"
@ -880,6 +881,13 @@ class Placeholder(Thing):
primaryjoin=binding_id == Device.id, primaryjoin=binding_id == Device.id,
) )
binding_id.comment = "binding placeholder with workbench device" 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): class Computer(Device):

View File

@ -310,11 +310,15 @@ class Sync:
c_placeholder = c.__class__(**c_dict) c_placeholder = c.__class__(**c_dict)
c_placeholder.parent = dev_placeholder c_placeholder.parent = dev_placeholder
c.parent = device 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(c_placeholder)
db.session.add(component_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(dev_placeholder)
db.session.add(placeholder) db.session.add(placeholder)

View File

@ -0,0 +1,185 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
<nav>
<ol class="breadcrumb">
<!-- TODO@slamora replace with lot list URL when exists -->
</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">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">{{ title }}</h5>
<p class="text-center">Please check that the information is correct.</p>
</div>
<table class="table table-hover">
<thead>
<tr class="text-center">
<th scope="col">Basic Data</th>
<th scope="col">Info to be Entered</th>
<th scope="col">Info to be Decoupled</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">PHID:</th>
<td class="table-success text-right">{{ placeholder.phid or '' }}</td>
<td class="table-danger">{{ device.placeholder.phid or '' }}</td>
</tr>
<tr>
<th scope="row">Manufacturer:</th>
<td class="table-success text-right">{{ placeholder.device.manufacturer or '' }}</td>
<td class="table-danger">{{ device.manufacturer or '' }}</td>
</tr>
<tr>
<th scope="row">Model:</th>
<td class="table-success">{{ placeholder.device.model or '' }}</td>
<td class="table-danger">{{ device.model or '' }}</td>
</tr>
<tr>
<th scope="row">Serial Number:</th>
<td class="table-success">{{ placeholder.device.serial_number or '' }}</td>
<td class="table-danger">{{ device.serial_number or '' }}</td>
</tr>
<tr>
<th scope="row">Brand:</th>
<td class="table-success">{{ placeholder.device.brand or '' }}</td>
<td class="table-danger">{{ device.brand or '' }}</td>
</tr>
<tr>
<th scope="row">Sku:</th>
<td class="table-success">{{ placeholder.device.sku or '' }}</td>
<td class="table-danger">{{ device.sku or '' }}</td>
</tr>
<tr>
<th scope="row">Generation:</th>
<td class="table-success">{{ placeholder.device.generation or '' }}</td>
<td class="table-danger">{{ device.generation or '' }}</td>
</tr>
<tr>
<th scope="row">Version:</th>
<td class="table-success">{{ placeholder.device.version or '' }}</td>
<td class="table-danger">{{ device.version or '' }}</td>
</tr>
<tr>
<th scope="row">Weight:</th>
<td class="table-success">{{ placeholder.device.weight or '' }}</td>
<td class="table-danger">{{ device.weight or '' }}</td>
</tr>
<tr>
<th scope="row">Width:</th>
<td class="table-success">{{ placeholder.device.width or '' }}</td>
<td class="table-danger">{{ device.width or '' }}</td>
</tr>
<tr>
<th scope="row">Height:</th>
<td class="table-success">{{ placeholder.device.height or '' }}</td>
<td class="table-danger">{{ device.height or '' }}</td>
</tr>
<tr>
<th scope="row">Depth:</th>
<td class="table-success">{{ placeholder.device.depth or '' }}</td>
<td class="table-danger">{{ device.depth or '' }}</td>
</tr>
<tr>
<th scope="row">Color:</th>
<td class="table-success">{{ placeholder.device.color or '' }}</td>
<td class="table-danger">{{ device.color or '' }}</td>
</tr>
<tr>
<th scope="row">Production date:</th>
<td class="table-success">{{ placeholder.device.production_date or '' }}</td>
<td class="table-danger">{{ device.production_date or '' }}</td>
</tr>
<tr>
<th scope="row">Variant:</th>
<td class="table-success">{{ placeholder.device.variant or '' }}</td>
<td class="table-danger">{{ device.variant or '' }}</td>
</tr>
</tbody>
</table>
<br />
{% if placeholder.device.components or device.components %}
<h2>Components</h2>
<table class="table table-hover">
<thead>
<tr class="text-center">
<th scope="col">Info to be Entered</th>
<th scope="col">Info to be Decoupled</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-success text-right">
{% for c in placeholder.device.components %}
* {{ c.verbose_name }}<br />
{% endfor %}
</td>
<td class="table-danger">
{% for c in device.components %}
* {{ c.verbose_name }}<br />
{% endfor %}
</td>
</tr>
</tbody>
</table>
{% endif %}
<br />
{% if placeholder.device.actions or device.actions %}
<h2>Actions</h2>
<table class="table table-hover">
<thead>
<tr class="text-center">
<th scope="col">Info to be Entered</th>
<th scope="col">Info to be Decoupled</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-success text-right">
{% for a in placeholder.device.actions %}
* {{ a.t }}<br />
{% endfor %}
</td>
<td class="table-danger">
{% for a in device.actions %}
* {{ a.t }}<br />
{% endfor %}
</td>
</tr>
</tbody>
</table>
{% endif %}
<div>
<form method="post">
<a href="{{ url_for('inventory.device_details', id=device.placeholder.binding.devicehub_id) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Confirm</button>
</form>
</div>
</div>
</div>
</div>
<div class="col-xl-8">
</div>
</div>
</section>
{% endblock main %}

View File

@ -62,10 +62,16 @@
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</button> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</button>
</li> </li>
{% if device.binding %}
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#binding">Binding</button>
</li>
{% endif %}
</ul> </ul>
<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 {% if active_binding %}profile-overview{% else %}show active{% endif %}" id="type">
<h5 class="card-title">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 %} {% if device.placeholder %}(<a href="{{ url_for('inventory.device_edit', id=device.devicehub_id)}}">edit</a>){% endif %}
@ -216,6 +222,42 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% if placeholder.binding %}
<div class="tab-pane fade {% if active_binding %}show active{% else %}profile-overview{% endif %}" id="binding">
<h5 class="card-title">Binding</h5>
<div class="list-group col-6">
<p>
Be careful, binding implies changes in the data of a device that affect its
traceability.
</p>
</div>
<div class="list-group col-6">
<form action="{{ url_for('inventory.device_details', id=placeholder.binding.devicehub_id) }}" method="post">
{{ form_binding.csrf_token }}
{% for field in form_binding %}
{% if field != form_binding.csrf_token %}
<div class="col-12">
{{ field.label(class_="form-label") }}:
{{ field }}
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="col-12 mt-2">
<input type="submit" class="btn btn-primary" value="Search" />
</div>
</form>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>

View File

@ -401,6 +401,8 @@
<th scope="col">Select</th> <th scope="col">Select</th>
<th scope="col">Title</th> <th scope="col">Title</th>
<th scope="col">DHID</th> <th scope="col">DHID</th>
<th scope="col">PHID</th>
<th scope="col">Is Abstract</th>
<th scope="col">Unique Identifiers</th> <th scope="col">Unique Identifiers</th>
<th scope="col">Lifecycle Status</th> <th scope="col">Lifecycle Status</th>
<th scope="col">Allocated Status</th> <th scope="col">Allocated Status</th>
@ -442,6 +444,12 @@
{{ dev.devicehub_id }} {{ dev.devicehub_id }}
</a> </a>
</td> </td>
<td>
{{ dev.binding and dev.binding.phid or dev.placeholder and dev.placeholder.phid or '' }}
</td>
<td>
{{ dev.binding and dev.binding.is_abstract and '✓' or dev.placeholder and dev.placeholder.is_abstract and '✓' or '' }}
</td>
<td> <td>
{% for t in dev.tags | sort(attribute="id") %} {% for t in dev.tags | sort(attribute="id") %}
<a href="{{ url_for('labels.label_details', id=t.id)}}">{{ t.id }}</a> <a href="{{ url_for('labels.label_details', id=t.id)}}">{{ t.id }}</a>