new device document

This commit is contained in:
Cayo Puigdefabregas 2023-03-31 18:06:22 +02:00
parent 4059dc3a7a
commit 645bdf3750
7 changed files with 438 additions and 1 deletions

View File

@ -32,6 +32,7 @@ from wtforms.fields import FormField
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.inventory.models import ( from ereuse_devicehub.inventory.models import (
DeliveryNote, DeliveryNote,
DeviceDocument,
ReceiverNote, ReceiverNote,
Transfer, Transfer,
TransferCustomerDetails, TransferCustomerDetails,
@ -1332,6 +1333,102 @@ class TradeDocumentForm(FlaskForm):
return self._obj return self._obj
class DeviceDocumentForm(FlaskForm):
url = URLField(
'Url',
[validators.Optional()],
render_kw={'class': "form-control"},
description="Url where the document resides",
)
description = StringField(
'Description',
[validators.Optional()],
render_kw={'class': "form-control"},
description="",
)
id_document = StringField(
'Document Id',
[validators.Optional()],
render_kw={'class': "form-control"},
description="Identification number of document",
)
type = StringField(
'Type',
[validators.Optional()],
render_kw={'class': "form-control"},
description="Type of document",
)
date = DateField(
'Date',
[validators.Optional()],
render_kw={'class': "form-control"},
description="",
)
file_name = FileField(
'File',
[validators.DataRequired()],
render_kw={'class': "form-control"},
description="""This file is not stored on our servers, it is only used to
generate a digital signature and obtain the name of the file.""",
)
def __init__(self, *args, **kwargs):
id = kwargs.pop('dhid')
doc_id = kwargs.pop('document', None)
self._device = Device.query.filter(Device.devicehub_id == id).first()
self._obj = None
if doc_id:
self._obj = DeviceDocument.query.filter_by(
id=doc_id, device=self._device, owner=g.user
).one()
kwargs['obj'] = self._obj
super().__init__(*args, **kwargs)
if self._obj:
if isinstance(self.url.data, URL):
self.url.data = self.url.data.to_text()
def validate(self, extra_validators=None):
is_valid = super().validate(extra_validators)
if g.user == self._device.owner:
is_valid = False
return is_valid
def save(self, commit=True):
file_name = ''
file_hash = ''
if self.file_name.data:
file_name = self.file_name.data.filename
file_hash = insert_hash(self.file_name.data.read(), commit=False)
self.url.data = URL(self.url.data)
if not self._obj:
self._obj = DeviceDocument(device_id=self._device.id)
self.populate_obj(self._obj)
self._obj.file_name = file_name
self._obj.file_hash = file_hash
if not self._obj.id:
db.session.add(self._obj)
self._device.documents.add(self._obj)
if commit:
db.session.commit()
return self._obj
def remove(self):
if self._obj:
self._obj.delete()
db.session.commit()
return self._obj
class TransferForm(FlaskForm): class TransferForm(FlaskForm):
lot_name = StringField( lot_name = StringField(
'Lot Name', 'Lot Name',

View File

@ -2,7 +2,8 @@ from uuid import uuid4
from citext import CIText from citext import CIText
from flask import g from flask import g
from sqlalchemy import Column, Integer from sortedcontainers import SortedSet
from sqlalchemy import BigInteger, Column, Integer
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import backref, relationship from sqlalchemy.orm import backref, relationship
from teal.db import CASCADE_OWN, URL from teal.db import CASCADE_OWN, URL
@ -110,3 +111,40 @@ class TransferCustomerDetails(Thing):
), ),
primaryjoin='TransferCustomerDetails.transfer_id == Transfer.id', primaryjoin='TransferCustomerDetails.transfer_id == Transfer.id',
) )
_sorted_documents = {
'order_by': lambda: DeviceDocument.created,
'collection_class': SortedSet,
}
class DeviceDocument(Thing):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
type = Column(db.CIText(), nullable=True)
date = Column(db.DateTime, nullable=True)
id_document = Column(db.CIText(), nullable=True)
description = Column(db.CIText(), nullable=True)
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)
device_id = db.Column(BigInteger, db.ForeignKey('device.id'), nullable=False)
device = db.relationship(
'Device',
primaryjoin='DeviceDocument.device_id == Device.id',
backref=backref(
'documents', lazy=True, cascade=CASCADE_OWN, **_sorted_documents
),
)
file_name = Column(db.CIText(), nullable=True)
file_hash = Column(db.CIText(), nullable=True)
url = db.Column(URL(), nullable=True)
# __table_args__ = (
# db.Index('document_id', id, postgresql_using='hash'),
# db.Index('type_doc', type, postgresql_using='hash')
# )

View File

@ -24,6 +24,7 @@ from ereuse_devicehub.inventory.forms import (
BindingForm, BindingForm,
CustomerDetailsForm, CustomerDetailsForm,
DataWipeForm, DataWipeForm,
DeviceDocumentForm,
EditTransferForm, EditTransferForm,
FilterForm, FilterForm,
LotForm, LotForm,
@ -568,6 +569,27 @@ class DocumentDeleteView(View):
return flask.redirect(next_url) return flask.redirect(next_url)
class DeviceDocumentDeleteView(View):
methods = ['GET']
decorators = [login_required]
template_name = 'inventory/device_list.html'
form_class = TradeDocumentForm
def dispatch_request(self, lot_id, doc_id):
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
form = self.form_class(lot=lot_id, document=doc_id)
try:
form.remove()
except Exception as err:
msg = "{}".format(err)
messages.error(msg)
return flask.redirect(next_url)
msg = "Document removed successfully."
messages.success(msg)
return flask.redirect(next_url)
class UploadSnapshotView(GenericMixin): class UploadSnapshotView(GenericMixin):
methods = ['GET', 'POST'] methods = ['GET', 'POST']
decorators = [login_required] decorators = [login_required]
@ -810,6 +832,27 @@ class NewTradeView(DeviceListMixin, NewActionView):
return flask.redirect(next_url) return flask.redirect(next_url)
class NewDeviceDocumentView(GenericMixin):
methods = ['POST', 'GET']
decorators = [login_required]
template_name = 'inventory/device_document.html'
form_class = DeviceDocumentForm
title = "Add new document"
def dispatch_request(self, dhid):
self.form = self.form_class(dhid=dhid)
self.get_context()
if self.form.validate_on_submit():
self.form.save()
messages.success('Document created successfully!')
next_url = url_for('inventory.device_details', id=dhid)
return flask.redirect(next_url)
self.context.update({'form': self.form, 'title': self.title})
return flask.render_template(self.template_name, **self.context)
class NewTradeDocumentView(GenericMixin): class NewTradeDocumentView(GenericMixin):
methods = ['POST', 'GET'] methods = ['POST', 'GET']
decorators = [login_required] decorators = [login_required]
@ -1554,6 +1597,10 @@ devices.add_url_rule(
devices.add_url_rule( devices.add_url_rule(
'/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add') '/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add')
) )
devices.add_url_rule(
'/device/<string:dhid>/document/add/',
view_func=NewDeviceDocumentView.as_view('device_document_add'),
)
devices.add_url_rule( devices.add_url_rule(
'/lot/<string:lot_id>/transfer-document/add/', '/lot/<string:lot_id>/transfer-document/add/',
view_func=NewTradeDocumentView.as_view('transfer_document_add'), view_func=NewTradeDocumentView.as_view('transfer_document_add'),

View File

@ -0,0 +1,100 @@
"""add document device
Revision ID: ac476b60d952
Revises: 4f33137586dd
Create Date: 2023-03-31 10:46:02.463007
"""
import citext
import sqlalchemy as sa
import teal
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'ac476b60d952'
down_revision = '4f33137586dd'
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(
'device_document',
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',
postgresql.UUID(as_uuid=True),
nullable=False,
),
sa.Column(
'type',
citext.CIText(),
nullable=True,
),
sa.Column(
'date',
sa.DateTime(),
nullable=True,
),
sa.Column(
'id_document',
citext.CIText(),
nullable=True,
),
sa.Column(
'description',
citext.CIText(),
nullable=True,
),
sa.Column('owner_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('device_id', sa.BigInteger(), nullable=False),
sa.Column(
'file_name',
citext.CIText(),
nullable=True,
),
sa.Column(
'file_hash',
citext.CIText(),
nullable=True,
),
sa.Column(
'url',
citext.CIText(),
teal.db.URL(),
nullable=True,
),
sa.ForeignKeyConstraint(
['device_id'],
[f'{get_inv()}.device.id'],
),
sa.ForeignKeyConstraint(
['owner_id'],
['common.user.id'],
),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
def downgrade():
op.drop_table('device_document', schema=f'{get_inv()}')

View File

@ -1232,6 +1232,13 @@ class Placeholder(Thing):
return 'Twin' return 'Twin'
return 'Placeholder' return 'Placeholder'
@property
def documents(self):
docs = self.device.documents
if self.binding:
return docs.union(self.binding.documents)
return docs
class Computer(Device): class Computer(Device):
"""A chassis with components inside that can be processed """A chassis with components inside that can be processed

View File

@ -65,6 +65,10 @@
<a class="nav-link" href="{{ device.public_link }}" target="_blank">Web</a> <a class="nav-link" href="{{ device.public_link }}" target="_blank">Web</a>
</li> </li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">Documents</button>
</li>
<li class="nav-item"> <li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</button> <button class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</button>
</li> </li>
@ -196,6 +200,80 @@
</div> </div>
</div> </div>
<div class="tab-pane fade profile-overview" id="documents">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="{{ url_for('inventory.device_document_add', dhid=placeholder.device.devicehub_id) }}" class="btn btn-primary">
<i class="bi bi-plus"></i>
Add new document
<span class="caret"></span>
</a>
</div>
<h5 class="card-title">Documents</h5>
<table class="table">
<thead>
<tr>
<th scope="col">File</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Uploaded on</th>
<th></th>
</tr>
</thead>
<tbody>
{% for doc in placeholder.documents %}
<tr>
<td>
{% if doc.get_url() %}
<a href="{{ doc.get_url() }}" target="_blank">{{ doc.file_name}}</a>
{% else %}
{{ doc.file_name}}
{% endif %}
</td>
<td>
{{ doc.created.strftime('%Y-%m-%d %H:%M')}}
</td>
<td>
<a href="{# url_for('inventory.transfer_document_edit', lot_id=lot.id, doc_id=doc.id)#" title="Edit document">
<i class="bi bi-pencil-square"></i>
</a>
</td>
<td>
<a href="javascript:javascript:void(0)" data-bs-toggle="modal" data-bs-target="#btnRemoveDocument{{ loop.index }}" title="Remove document">
<i class="bi bi-trash-fill"></i>
</a>
<div class="modal fade" id="btnRemoveDocument{{ loop.index }}" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Document</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure that you want to delete this Document?<br />
<strong>{{ doc.file_name }}</strong>
<p class="text-danger">
This action cannot be undone.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary-outline" data-bs-dismiss="modal">Cancel</button>
<a href="{# url_for('inventory.document_del', lot_id=lot.id, doc_id=doc.id) #}" type="button" class="btn btn-danger">
Delete it!
</a>
</div>
</div>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-pane fade profile-overview" id="status"> <div class="tab-pane fade profile-overview" id="status">
<h5 class="card-title">Status Details</h5> <h5 class="card-title">Status Details</h5>
<div class="row"> <div class="row">

View File

@ -0,0 +1,70 @@
{% extends "ereuse_devicehub/base_site.html" %}
{% block main %}
<div class="pagetitle">
<h1>{{ title }}</h1>
<nav>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#TODO-lot-list">Inventory</a></li>
<li class="breadcrumb-item"><a href="#TODO-lot-list">Device {{ form._device.dhid }}</a></li>
<li class="breadcrumb-item">Document</li>
</ol>
</nav>
</div><!-- End Page Title -->
<section class="section profile">
<div class="row">
<div class="col-xl-4">
<div class="card">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">{{ title }}</h5>
{% if form.form_errors %}
<p class="text-danger">
{% for error in form.form_errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% if form._obj or 1==2 %}
<form action="{{ url_for('inventory.device_document_edit', dhid=form._device.dhid, doc_id=form._obj.id) }}" method="post"
class="row g-3 needs-validation" enctype="multipart/form-data">
{% else %}
<form action="{{ url_for('inventory.device_document_add', dhid=form._device.dhid) }}" method="post"
class="row g-3 needs-validation" enctype="multipart/form-data">
{% endif %}
{{ form.csrf_token }}
{% for field in form %}
{% if field != form.csrf_token %}
<div>
{{ field.label(class_="form-label") }}
{{ field }}
<small class="text-muted">{{ field.description }}</small>
{% if field.errors %}
<p class="text-danger">
{% for error in field.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
<div>
<a href="{{ url_for('inventory.device_details', id=form._device.dhid) }}" class="btn btn-danger">Cancel</a>
<button class="btn btn-primary" type="submit">Save</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock main %}