Merge branch 'testing' into without-teal

This commit is contained in:
Cayo Puigdefabregas 2023-04-17 11:51:44 +02:00
commit ae5992f4c0
13 changed files with 728 additions and 26 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,
@ -110,6 +111,15 @@ DEVICES = {
"Other Devices": ["Other"], "Other Devices": ["Other"],
} }
TYPES_DOCUMENTS = [
("", ""),
("image", "Image"),
("main_image", "Main Image"),
("functionality_report", "Functionality Report"),
("data_sanitization_report", "Data Sanitization Report"),
("disposition_report", "Disposition Report"),
]
COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer'] COMPUTERS = ['Desktop', 'Laptop', 'Server', 'Computer']
MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"] MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"]
@ -1274,8 +1284,20 @@ class TradeDocumentForm(FlaskForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
lot_id = kwargs.pop('lot') lot_id = kwargs.pop('lot')
super().__init__(*args, **kwargs) doc_id = kwargs.pop('document', None)
self._lot = Lot.query.filter(Lot.id == lot_id).one() self._lot = Lot.query.filter(Lot.id == lot_id).one()
self._obj = None
if doc_id:
self._obj = TradeDocument.query.filter_by(
id=doc_id, lot=self._lot, 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()
if not self._lot.transfer: if not self._lot.transfer:
self.form_errors = ['Error, this lot is not a transfer lot.'] self.form_errors = ['Error, this lot is not a transfer lot.']
@ -1296,17 +1318,126 @@ class TradeDocumentForm(FlaskForm):
file_hash = insert_hash(self.file_name.data.read(), commit=False) file_hash = insert_hash(self.file_name.data.read(), commit=False)
self.url.data = URL(self.url.data) self.url.data = URL(self.url.data)
self._obj = TradeDocument(lot_id=self._lot.id) if not self._obj:
self._obj = TradeDocument(lot_id=self._lot.id)
self.populate_obj(self._obj) self.populate_obj(self._obj)
self._obj.file_name = file_name self._obj.file_name = file_name
self._obj.file_hash = file_hash self._obj.file_hash = file_hash
db.session.add(self._obj)
self._lot.documents.add(self._obj) if not self._obj.id:
db.session.add(self._obj)
self._lot.documents.add(self._obj)
if commit: if commit:
db.session.commit() db.session.commit()
return self._obj return self._obj
def remove(self):
if self._obj:
self._obj.delete()
db.session.commit()
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 = SelectField(
'Type',
[validators.Optional()],
choices=TYPES_DOCUMENTS,
default="",
render_kw={'class': "form-select"},
)
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(

View File

@ -1,8 +1,10 @@
from uuid import uuid4 from uuid import uuid4
from citext import CIText from citext import CIText
from dateutil.tz import tzutc
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
@ -110,3 +112,50 @@ 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')
# )
def get_url(self) -> str:
if self.url:
return self.url.to_text()
return ''
def __lt__(self, other):
return self.created.replace(tzinfo=tzutc()) < other.created.replace(
tzinfo=tzutc()
)

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,
@ -547,6 +548,27 @@ class LotDeleteView(View):
return flask.redirect(next_url) return flask.redirect(next_url)
class DocumentDeleteView(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]
@ -789,6 +811,69 @@ 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 EditDeviceDocumentView(GenericMixin):
decorators = [login_required]
methods = ['POST', 'GET']
template_name = 'inventory/device_document.html'
form_class = DeviceDocumentForm
title = "Edit document"
def dispatch_request(self, dhid, doc_id):
self.form = self.form_class(dhid=dhid, document=doc_id)
self.get_context()
if self.form.validate_on_submit():
self.form.save()
messages.success('Edit document 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 DeviceDocumentDeleteView(View):
methods = ['GET']
decorators = [login_required]
template_name = 'inventory/device_detail.html'
form_class = DeviceDocumentForm
def dispatch_request(self, dhid, doc_id):
self.form = self.form_class(dhid=dhid, document=doc_id)
next_url = url_for('inventory.device_details', id=dhid)
try:
self.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 NewTradeDocumentView(GenericMixin): class NewTradeDocumentView(GenericMixin):
methods = ['POST', 'GET'] methods = ['POST', 'GET']
decorators = [login_required] decorators = [login_required]
@ -810,6 +895,27 @@ class NewTradeDocumentView(GenericMixin):
return flask.render_template(self.template_name, **self.context) return flask.render_template(self.template_name, **self.context)
class EditTransferDocumentView(GenericMixin):
decorators = [login_required]
methods = ['POST', 'GET']
template_name = 'inventory/trade_document.html'
form_class = TradeDocumentForm
title = "Edit document"
def dispatch_request(self, lot_id, doc_id):
self.form = self.form_class(lot=lot_id, document=doc_id)
self.get_context()
if self.form.validate_on_submit():
self.form.save()
messages.success('Edit document successfully!')
next_url = url_for('inventory.lotdevicelist', lot_id=lot_id)
return flask.redirect(next_url)
self.context.update({'form': self.form, 'title': self.title})
return flask.render_template(self.template_name, **self.context)
class NewTransferView(GenericMixin): class NewTransferView(GenericMixin):
methods = ['POST', 'GET'] methods = ['POST', 'GET']
template_name = 'inventory/new_transfer.html' template_name = 'inventory/new_transfer.html'
@ -1512,8 +1618,28 @@ 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( devices.add_url_rule(
'/lot/<string:lot_id>/trade-document/add/', '/device/<string:dhid>/document/add/',
view_func=NewTradeDocumentView.as_view('trade_document_add'), view_func=NewDeviceDocumentView.as_view('device_document_add'),
)
devices.add_url_rule(
'/device/<string:dhid>/document/edit/<string:doc_id>',
view_func=EditDeviceDocumentView.as_view('device_document_edit'),
)
devices.add_url_rule(
'/device/<string:dhid>/document/del/<string:doc_id>',
view_func=DeviceDocumentDeleteView.as_view('device_document_del'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/transfer-document/add/',
view_func=NewTradeDocumentView.as_view('transfer_document_add'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/document/edit/<string:doc_id>',
view_func=EditTransferDocumentView.as_view('transfer_document_edit'),
)
devices.add_url_rule(
'/lot/<string:lot_id>/document/del/<string:doc_id>',
view_func=DocumentDeleteView.as_view('document_del'),
) )
devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist'))
devices.add_url_rule( devices.add_url_rule(

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

@ -1,4 +1,5 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from flask_sqlalchemy import event from flask_sqlalchemy import event
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
@ -16,18 +17,23 @@ class Thing(db.Model):
`schema.org's Thing class <https://schema.org/Thing>`_ `schema.org's Thing class <https://schema.org/Thing>`_
using only needed fields. using only needed fields.
""" """
__abstract__ = True __abstract__ = True
updated = db.Column(db.TIMESTAMP(timezone=True), updated = db.Column(
nullable=False, db.TIMESTAMP(timezone=True),
index=True, nullable=False,
server_default=db.text('CURRENT_TIMESTAMP')) index=True,
server_default=db.text('CURRENT_TIMESTAMP'),
)
updated.comment = """The last time Devicehub recorded a change for updated.comment = """The last time Devicehub recorded a change for
this thing. this thing.
""" """
created = db.Column(db.TIMESTAMP(timezone=True), created = db.Column(
nullable=False, db.TIMESTAMP(timezone=True),
index=True, nullable=False,
server_default=db.text('CURRENT_TIMESTAMP')) index=True,
server_default=db.text('CURRENT_TIMESTAMP'),
)
created.comment = """When Devicehub created this.""" created.comment = """When Devicehub created this."""
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
@ -36,11 +42,15 @@ class Thing(db.Model):
self.created = kwargs.get('created', datetime.now(timezone.utc)) self.created = kwargs.get('created', datetime.now(timezone.utc))
super().__init__(**kwargs) super().__init__(**kwargs)
def delete(self):
db.session.delete(self)
def update_object_timestamp(mapper, connection, thing_obj): def update_object_timestamp(mapper, connection, thing_obj):
""" This function update the stamptime of field updated """ """This function update the stamptime of field updated"""
thing_obj.updated = datetime.now(timezone.utc) thing_obj.updated = datetime.now(timezone.utc)
def listener_reset_field_updated_in_actual_time(thing_obj): def listener_reset_field_updated_in_actual_time(thing_obj):
""" This function launch a event than listen like a signal when some object is saved """ """This function launch a event than listen like a signal when some object is saved"""
event.listen(thing_obj, 'before_update', update_object_timestamp, propagate=True) event.listen(thing_obj, 'before_update', update_object_timestamp, propagate=True)

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,81 @@
</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>
<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.device_document_edit', dhid=doc.device.dhid, 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.device_document_del', dhid=doc.device.dhid, 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 %}

View File

@ -517,7 +517,7 @@
{% if lot and not lot.is_temporary %} {% if lot and not lot.is_temporary %}
<div id="trade-documents-list" class="tab-pane fade trade-documents-list"> <div id="trade-documents-list" class="tab-pane fade trade-documents-list">
<div class="btn-group dropdown ml-1 mt-1" uib-dropdown=""> <div class="btn-group dropdown ml-1 mt-1" uib-dropdown="">
<a href="{{ url_for('inventory.trade_document_add', lot_id=lot.id)}}" class="btn btn-primary"> <a href="{{ url_for('inventory.transfer_document_add', lot_id=lot.id)}}" class="btn btn-primary">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Add new document Add new document
<span class="caret"></span> <span class="caret"></span>
@ -530,6 +530,7 @@
<tr> <tr>
<th scope="col">File</th> <th scope="col">File</th>
<th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Uploaded on</th> <th scope="col" data-type="date" data-format="YYYY-MM-DD hh:mm">Uploaded on</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -545,6 +546,43 @@
<td> <td>
{{ doc.created.strftime('%Y-%m-%d %H:%M')}} {{ doc.created.strftime('%Y-%m-%d %H:%M')}}
</td> </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> </tr>
{% endfor %} {% endfor %}
{% for doc in lot.trade.documents %} {% for doc in lot.trade.documents %}
@ -559,6 +597,9 @@
<td> <td>
{{ doc.created.strftime('%Y-%m-%d %H:%M')}} {{ doc.created.strftime('%Y-%m-%d %H:%M')}}
</td> </td>
<td>
<a href="javascript:void(0)"><i class="bi bi-trash-fill"></i></a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -254,7 +254,7 @@
</button> </button>
<ul class="dropdown-menu" aria-labelledby="btnSnapshot"> <ul class="dropdown-menu" aria-labelledby="btnSnapshot">
<li> <li>
<a href="{{ url_for('inventory.trade_document_add', lot_id=lot.id)}}" class="dropdown-item"> <a href="{{ url_for('inventory.transfer_document_add', lot_id=lot.id)}}" class="dropdown-item">
<i class="bi bi-plus"></i> <i class="bi bi-plus"></i>
Add new document Add new document
<span class="caret"></span> <span class="caret"></span>

View File

@ -30,8 +30,13 @@
{% endif %} {% endif %}
</div> </div>
<form action="{{ url_for('inventory.trade_document_add', lot_id=form._lot.id) }}" method="post" {% if form._obj %}
<form action="{{ url_for('inventory.transfer_document_edit', lot_id=form._lot.id, doc_id=form._obj.id) }}" method="post"
class="row g-3 needs-validation" enctype="multipart/form-data"> class="row g-3 needs-validation" enctype="multipart/form-data">
{% else %}
<form action="{{ url_for('inventory.transfer_document_add', lot_id=form._lot.id) }}" method="post"
class="row g-3 needs-validation" enctype="multipart/form-data">
{% endif %}
{{ form.csrf_token }} {{ form.csrf_token }}
{% for field in form %} {% for field in form %}
{% if field != form.csrf_token %} {% if field != form.csrf_token %}

View File

@ -55,6 +55,9 @@ def test_api_docs(client: Client):
'/inventory/device/add/', '/inventory/device/add/',
'/inventory/device/{id}/', '/inventory/device/{id}/',
'/inventory/device/{dhid}/binding/', '/inventory/device/{dhid}/binding/',
'/inventory/device/{dhid}/document/del/{doc_id}',
'/inventory/device/{dhid}/document/edit/{doc_id}',
'/inventory/device/{dhid}/document/add/',
'/inventory/device/erasure/', '/inventory/device/erasure/',
'/inventory/device/erasure/{orphans}/', '/inventory/device/erasure/{orphans}/',
'/inventory/all/device/', '/inventory/all/device/',
@ -66,13 +69,15 @@ def test_api_docs(client: Client):
'/inventory/lot/{lot_id}/device/add/', '/inventory/lot/{lot_id}/device/add/',
'/inventory/lot/{lot_id}/deliverynote/', '/inventory/lot/{lot_id}/deliverynote/',
'/inventory/lot/{lot_id}/receivernote/', '/inventory/lot/{lot_id}/receivernote/',
'/inventory/lot/{lot_id}/trade-document/add/', '/inventory/lot/{lot_id}/transfer-document/add/',
'/inventory/lot/{lot_id}/transfer/{type_id}/', '/inventory/lot/{lot_id}/transfer/{type_id}/',
'/inventory/lot/{lot_id}/opentransfer/', '/inventory/lot/{lot_id}/opentransfer/',
'/inventory/lot/{lot_id}/transfer/', '/inventory/lot/{lot_id}/transfer/',
'/inventory/lot/transfer/{type_id}/', '/inventory/lot/transfer/{type_id}/',
'/inventory/lot/{lot_id}/upload-snapshot/', '/inventory/lot/{lot_id}/upload-snapshot/',
'/inventory/lot/{lot_id}/customerdetails/', '/inventory/lot/{lot_id}/customerdetails/',
'/inventory/lot/{lot_id}/document/edit/{doc_id}',
'/inventory/lot/{lot_id}/document/del/{doc_id}',
'/inventory/snapshots/{snapshot_uuid}/', '/inventory/snapshots/{snapshot_uuid}/',
'/inventory/snapshots/', '/inventory/snapshots/',
'/inventory/tag/devices/{dhid}/add/', '/inventory/tag/devices/{dhid}/add/',

View File

@ -2468,7 +2468,7 @@ def test_bug_3831_documents(user3: UserClientFlask):
lot = Lot.query.filter_by(name=lot_name).one() lot = Lot.query.filter_by(name=lot_name).one()
lot_id = lot.id lot_id = lot.id
uri = f'/inventory/lot/{lot_id}/trade-document/add/' uri = f'/inventory/lot/{lot_id}/transfer-document/add/'
body, status = user3.get(uri) body, status = user3.get(uri)
txt = 'Error, this lot is not a transfer lot.' txt = 'Error, this lot is not a transfer lot.'
@ -2486,7 +2486,7 @@ def test_bug_3831_documents(user3: UserClientFlask):
assert 'Incoming Lot' in body assert 'Incoming Lot' in body
lot_id = Lot.query.all()[1].id lot_id = Lot.query.all()[1].id
uri = f'/inventory/lot/{lot_id}/trade-document/add/' uri = f'/inventory/lot/{lot_id}/transfer-document/add/'
body, status = user3.get(uri) body, status = user3.get(uri)
b_file = b'1234567890' b_file = b'1234567890'
@ -2502,12 +2502,12 @@ def test_bug_3831_documents(user3: UserClientFlask):
'file': file_upload, 'file': file_upload,
} }
uri = f'/inventory/lot/{lot_id}/trade-document/add/' uri = f'/inventory/lot/{lot_id}/transfer-document/add/'
body, status = user3.post(uri, data=data, content_type="multipart/form-data") body, status = user3.post(uri, data=data, content_type="multipart/form-data")
assert status == '200 OK' assert status == '200 OK'
# Second document # Second document
uri = f'/inventory/lot/{lot_id}/trade-document/add/' uri = f'/inventory/lot/{lot_id}/transfer-document/add/'
file_upload = (BytesIO(b_file), file_name) file_upload = (BytesIO(b_file), file_name)
data['file'] = file_upload data['file'] = file_upload
data['csrf_token'] = generate_csrf() data['csrf_token'] = generate_csrf()
@ -2774,3 +2774,82 @@ def test_reliable_device(user3: UserClientFlask):
assert Snapshot.query.first() == snapshot assert Snapshot.query.first() == snapshot
assert len(snapshot.device.components) == 8 assert len(snapshot.device.components) == 8
assert len(snapshot.device.actions) == 7 assert len(snapshot.device.actions) == 7
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_add_device_document(user3: UserClientFlask):
snapshot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
device = Device.query.filter_by(devicehub_id=snapshot.device.dhid).one()
uri = '/inventory/device/{}/document/add/'.format(device.dhid)
user3.get(uri)
name = "doc1.pdf"
url = "https://www.usody.com/"
file_name = (BytesIO(b'1234567890'), name)
data = {
'url': url,
'file_name': file_name,
'csrf_token': generate_csrf(),
}
user3.post(uri, data=data, content_type="multipart/form-data")
assert device.documents[0].file_name == name
assert device.documents[0].url.to_text() == url
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_edit_device_document(user3: UserClientFlask):
snapshot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
device = Device.query.filter_by(devicehub_id=snapshot.device.dhid).one()
uri = '/inventory/device/{}/document/add/'.format(device.dhid)
user3.get(uri)
name = "doc1.pdf"
url = "https://www.usody.com/"
file_name = (BytesIO(b'1234567890'), name)
data = {
'url': url,
'file_name': file_name,
'csrf_token': generate_csrf(),
}
user3.post(uri, data=data, content_type="multipart/form-data")
doc_id = str(device.documents[0].id)
uri = '/inventory/device/{}/document/edit/{}'.format(device.dhid, doc_id)
user3.get(uri)
data['url'] = "https://www.ereuse.org/"
data['csrf_token'] = generate_csrf()
data['file_name'] = (BytesIO(b'1234567890'), name)
user3.post(uri, data=data, content_type="multipart/form-data")
assert device.documents[0].file_name == name
assert device.documents[0].url.to_text() == data['url']
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_delete_device_document(user3: UserClientFlask):
snapshot = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
device = Device.query.filter_by(devicehub_id=snapshot.device.dhid).one()
uri = '/inventory/device/{}/document/add/'.format(device.dhid)
user3.get(uri)
name = "doc1.pdf"
url = "https://www.usody.com/"
file_name = (BytesIO(b'1234567890'), name)
data = {
'url': url,
'file_name': file_name,
'csrf_token': generate_csrf(),
}
user3.post(uri, data=data, content_type="multipart/form-data")
doc_id = str(device.documents[0].id)
uri = '/inventory/device/{}/document/del/{}'.format(device.dhid, doc_id)
user3.get(uri)
assert len(device.documents) == 0