diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index ffb8d13c..bbc0ea1b 100644 --- a/ereuse_devicehub/inventory/forms.py +++ b/ereuse_devicehub/inventory/forms.py @@ -32,6 +32,7 @@ from wtforms.fields import FormField from ereuse_devicehub.db import db from ereuse_devicehub.inventory.models import ( DeliveryNote, + DeviceDocument, ReceiverNote, Transfer, TransferCustomerDetails, @@ -110,6 +111,15 @@ DEVICES = { "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'] MONITORS = ["ComputerMonitor", "Monitor", "TelevisionSet", "Projector"] @@ -1274,8 +1284,20 @@ class TradeDocumentForm(FlaskForm): def __init__(self, *args, **kwargs): 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._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: 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) 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._obj.file_name = file_name 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: db.session.commit() 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): lot_name = StringField( diff --git a/ereuse_devicehub/inventory/models.py b/ereuse_devicehub/inventory/models.py index 83fd6619..ace7da19 100644 --- a/ereuse_devicehub/inventory/models.py +++ b/ereuse_devicehub/inventory/models.py @@ -1,8 +1,10 @@ from uuid import uuid4 from citext import CIText +from dateutil.tz import tzutc 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.orm import backref, relationship @@ -110,3 +112,50 @@ class TransferCustomerDetails(Thing): ), 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() + ) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index 79145f3c..bb1ac9a9 100644 --- a/ereuse_devicehub/inventory/views.py +++ b/ereuse_devicehub/inventory/views.py @@ -24,6 +24,7 @@ from ereuse_devicehub.inventory.forms import ( BindingForm, CustomerDetailsForm, DataWipeForm, + DeviceDocumentForm, EditTransferForm, FilterForm, LotForm, @@ -547,6 +548,27 @@ class LotDeleteView(View): 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): methods = ['GET', 'POST'] decorators = [login_required] @@ -789,6 +811,69 @@ class NewTradeView(DeviceListMixin, NewActionView): 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): methods = ['POST', 'GET'] decorators = [login_required] @@ -810,6 +895,27 @@ class NewTradeDocumentView(GenericMixin): 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): methods = ['POST', 'GET'] template_name = 'inventory/new_transfer.html' @@ -1512,8 +1618,28 @@ devices.add_url_rule( '/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add') ) devices.add_url_rule( - '/lot//trade-document/add/', - view_func=NewTradeDocumentView.as_view('trade_document_add'), + '/device//document/add/', + view_func=NewDeviceDocumentView.as_view('device_document_add'), +) +devices.add_url_rule( + '/device//document/edit/', + view_func=EditDeviceDocumentView.as_view('device_document_edit'), +) +devices.add_url_rule( + '/device//document/del/', + view_func=DeviceDocumentDeleteView.as_view('device_document_del'), +) +devices.add_url_rule( + '/lot//transfer-document/add/', + view_func=NewTradeDocumentView.as_view('transfer_document_add'), +) +devices.add_url_rule( + '/lot//document/edit/', + view_func=EditTransferDocumentView.as_view('transfer_document_edit'), +) +devices.add_url_rule( + '/lot//document/del/', + view_func=DocumentDeleteView.as_view('document_del'), ) devices.add_url_rule('/device/', view_func=DeviceListView.as_view('devicelist')) devices.add_url_rule( diff --git a/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py b/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py new file mode 100644 index 00000000..359f0b57 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py @@ -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()}') diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 567a5bc9..0c1bd082 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -1232,6 +1232,13 @@ class Placeholder(Thing): return 'Twin' return 'Placeholder' + @property + def documents(self): + docs = self.device.documents + if self.binding: + return docs.union(self.binding.documents) + return docs + class Computer(Device): """A chassis with components inside that can be processed diff --git a/ereuse_devicehub/resources/models.py b/ereuse_devicehub/resources/models.py index e079269f..485fc96e 100644 --- a/ereuse_devicehub/resources/models.py +++ b/ereuse_devicehub/resources/models.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone + from flask_sqlalchemy import event from ereuse_devicehub.db import db @@ -16,18 +17,23 @@ class Thing(db.Model): `schema.org's Thing class `_ using only needed fields. """ + __abstract__ = True - updated = db.Column(db.TIMESTAMP(timezone=True), - nullable=False, - index=True, - server_default=db.text('CURRENT_TIMESTAMP')) - updated.comment = """The last time Devicehub recorded a change for + updated = db.Column( + db.TIMESTAMP(timezone=True), + nullable=False, + index=True, + server_default=db.text('CURRENT_TIMESTAMP'), + ) + updated.comment = """The last time Devicehub recorded a change for this thing. """ - created = db.Column(db.TIMESTAMP(timezone=True), - nullable=False, - index=True, - server_default=db.text('CURRENT_TIMESTAMP')) + created = db.Column( + db.TIMESTAMP(timezone=True), + nullable=False, + index=True, + server_default=db.text('CURRENT_TIMESTAMP'), + ) created.comment = """When Devicehub created this.""" def __init__(self, **kwargs) -> None: @@ -36,11 +42,15 @@ class Thing(db.Model): self.created = kwargs.get('created', datetime.now(timezone.utc)) super().__init__(**kwargs) + def delete(self): + db.session.delete(self) + 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) + 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) diff --git a/ereuse_devicehub/templates/inventory/device_detail.html b/ereuse_devicehub/templates/inventory/device_detail.html index 762f7cb1..c5e65e37 100644 --- a/ereuse_devicehub/templates/inventory/device_detail.html +++ b/ereuse_devicehub/templates/inventory/device_detail.html @@ -65,6 +65,10 @@ Web + + @@ -196,6 +200,81 @@ +
+ + +
Documents
+ + + + + + + + + + + {% for doc in placeholder.documents %} + + + + + + + {% endfor %} + +
FileUploaded on
+ {% if doc.get_url() %} + {{ doc.file_name}} + {% else %} + {{ doc.file_name}} + {% endif %} + + {{ doc.created.strftime('%Y-%m-%d %H:%M')}} + + + + + + + + + +
+
+
Status Details
diff --git a/ereuse_devicehub/templates/inventory/device_document.html b/ereuse_devicehub/templates/inventory/device_document.html new file mode 100644 index 00000000..1991444f --- /dev/null +++ b/ereuse_devicehub/templates/inventory/device_document.html @@ -0,0 +1,70 @@ +{% extends "ereuse_devicehub/base_site.html" %} +{% block main %} + +
+

{{ title }}

+ +
+ +
+
+
+ +
+
+ +
+
{{ title }}
+ {% if form.form_errors %} +

+ {% for error in form.form_errors %} + {{ error }}
+ {% endfor %} +

+ {% endif %} +
+ + {% if form._obj or 1==2 %} +
+ {% else %} + + {% endif %} + {{ form.csrf_token }} + {% for field in form %} + {% if field != form.csrf_token %} +
+ {{ field.label(class_="form-label") }} + {{ field }} + {{ field.description }} + {% if field.errors %} +

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

+ {% endif %} +
+ {% endif %} + {% endfor %} + +
+ Cancel + +
+
+ +
+
+ +
+
+
+{% endblock main %} diff --git a/ereuse_devicehub/templates/inventory/device_list.html b/ereuse_devicehub/templates/inventory/device_list.html index 2e2bc7b6..cc304687 100644 --- a/ereuse_devicehub/templates/inventory/device_list.html +++ b/ereuse_devicehub/templates/inventory/device_list.html @@ -517,7 +517,7 @@ {% if lot and not lot.is_temporary %}
-
+ {% else %} + + {% endif %} {{ form.csrf_token }} {% for field in form %} {% if field != form.csrf_token %} diff --git a/tests/test_basic.py b/tests/test_basic.py index 4b734079..bf78792d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -55,6 +55,9 @@ def test_api_docs(client: Client): '/inventory/device/add/', '/inventory/device/{id}/', '/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/{orphans}/', '/inventory/all/device/', @@ -66,13 +69,15 @@ def test_api_docs(client: Client): '/inventory/lot/{lot_id}/device/add/', '/inventory/lot/{lot_id}/deliverynote/', '/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}/opentransfer/', '/inventory/lot/{lot_id}/transfer/', '/inventory/lot/transfer/{type_id}/', '/inventory/lot/{lot_id}/upload-snapshot/', '/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/', '/inventory/tag/devices/{dhid}/add/', diff --git a/tests/test_render_2_0.py b/tests/test_render_2_0.py index 6cb43f31..d2842429 100644 --- a/tests/test_render_2_0.py +++ b/tests/test_render_2_0.py @@ -2468,7 +2468,7 @@ def test_bug_3831_documents(user3: UserClientFlask): lot = Lot.query.filter_by(name=lot_name).one() 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) 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 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) b_file = b'1234567890' @@ -2502,12 +2502,12 @@ def test_bug_3831_documents(user3: UserClientFlask): '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") assert status == '200 OK' # 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) data['file'] = file_upload data['csrf_token'] = generate_csrf() @@ -2774,3 +2774,82 @@ def test_reliable_device(user3: UserClientFlask): assert Snapshot.query.first() == snapshot assert len(snapshot.device.components) == 8 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