From 645bdf37506506c59fe9d451bf67cc0ce9338cce Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 31 Mar 2023 18:06:22 +0200 Subject: [PATCH] new device document --- ereuse_devicehub/inventory/forms.py | 97 +++++++++++++++++ ereuse_devicehub/inventory/models.py | 40 ++++++- ereuse_devicehub/inventory/views.py | 47 ++++++++ .../ac476b60d952_add_document_device.py | 100 ++++++++++++++++++ ereuse_devicehub/resources/device/models.py | 7 ++ .../templates/inventory/device_detail.html | 78 ++++++++++++++ .../templates/inventory/device_document.html | 70 ++++++++++++ 7 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 ereuse_devicehub/migrations/versions/ac476b60d952_add_document_device.py create mode 100644 ereuse_devicehub/templates/inventory/device_document.html diff --git a/ereuse_devicehub/inventory/forms.py b/ereuse_devicehub/inventory/forms.py index bd5c5498..cc1fd447 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, @@ -1332,6 +1333,102 @@ class TradeDocumentForm(FlaskForm): 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): lot_name = StringField( 'Lot Name', diff --git a/ereuse_devicehub/inventory/models.py b/ereuse_devicehub/inventory/models.py index f8b4f977..7073078e 100644 --- a/ereuse_devicehub/inventory/models.py +++ b/ereuse_devicehub/inventory/models.py @@ -2,7 +2,8 @@ from uuid import uuid4 from citext import CIText 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 from teal.db import CASCADE_OWN, URL @@ -110,3 +111,40 @@ 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') + # ) diff --git a/ereuse_devicehub/inventory/views.py b/ereuse_devicehub/inventory/views.py index a1481399..e6906564 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, @@ -568,6 +569,27 @@ class DocumentDeleteView(View): 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): methods = ['GET', 'POST'] decorators = [login_required] @@ -810,6 +832,27 @@ 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 NewTradeDocumentView(GenericMixin): methods = ['POST', 'GET'] decorators = [login_required] @@ -1554,6 +1597,10 @@ devices.add_url_rule( devices.add_url_rule( '/action/datawipe/add/', view_func=NewDataWipeView.as_view('datawipe_add') ) +devices.add_url_rule( + '/device//document/add/', + view_func=NewDeviceDocumentView.as_view('device_document_add'), +) devices.add_url_rule( '/lot//transfer-document/add/', view_func=NewTradeDocumentView.as_view('transfer_document_add'), 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 b0a70c81..f8183e13 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/templates/inventory/device_detail.html b/ereuse_devicehub/templates/inventory/device_detail.html index 762f7cb1..a7025549 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,80 @@ +
+ + +
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 %}