Merge pull request #450 from eReuse/feature/4357-add-datawipe-csv

Feature/4357 add datawipe csv
This commit is contained in:
cayop 2023-05-29 09:25:46 +02:00 committed by GitHub
commit a42dfe5469
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 324 additions and 112 deletions

View file

@ -52,6 +52,7 @@ from ereuse_devicehub.resources.device.models import (
Cellphone, Cellphone,
Computer, Computer,
ComputerMonitor, ComputerMonitor,
DataStorage,
Desktop, Desktop,
Device, Device,
Keyboard, Keyboard,
@ -851,7 +852,13 @@ class NewActionForm(ActionFormMixin):
if not is_valid: if not is_valid:
return False return False
if self.type.data in ['Allocate', 'Deallocate', 'Trade', 'DataWipe']: if self.type.data in [
'Allocate',
'Deallocate',
'Trade',
'DataWipe',
'EraseDataWipe',
]:
return False return False
return True return True
@ -1067,15 +1074,26 @@ class DataWipeForm(ActionFormMixin):
Model = db.Model._decl_class_registry.data[self.type.data]() Model = db.Model._decl_class_registry.data[self.type.data]()
self.instance = Model() self.instance = Model()
devices = self.devices.data devices = self.devices.data
if not self.document.success.data:
self.severity.data = Severity.Error.name
severity = self.severity.data severity = self.severity.data
self.devices.data = self._devices self.devices.data = self._devices
self.severity.data = Severity[self.severity.data] self.severity.data = Severity[self.severity.data]
document = copy.copy(self.document) document = copy.copy(self.document)
del self.document del self.document
self.populate_obj(self.instance) for dev in self._devices:
self.instance.document = document.form._obj ac = None
db.session.add(self.instance) for hd in dev.components:
if not isinstance(hd, DataStorage):
continue
ac = Model()
self.populate_obj(ac)
ac.parent = dev
ac.device = hd
ac.device_id = hd.id
ac.document = document.form._obj
db.session.add(ac)
db.session.commit() db.session.commit()
self.devices.data = devices self.devices.data = devices

View file

@ -1176,9 +1176,9 @@ class ExportsView(View):
row = [ row = [
ac.device.serial_number.upper(), ac.device.serial_number.upper(),
ac.device.dhid, ac.device.dhid,
ac.snapshot.uuid, ac.snapshot.uuid if ac.snapshot else '',
ac.type, ac.type,
ac.get_phid(), ac.parent.phid() if ac.parent else '',
ac.severity, ac.severity,
ac.created.strftime('%Y-%m-%d %H:%M:%S'), ac.created.strftime('%Y-%m-%d %H:%M:%S'),
] ]
@ -1192,11 +1192,12 @@ class ExportsView(View):
if device.placeholder and device.placeholder.binding: if device.placeholder and device.placeholder.binding:
device = device.placeholder.binding device = device.placeholder.binding
if isinstance(device, Computer): if isinstance(device, Computer):
for privacy in device.privacy: for ac in device.last_erase_action:
erasures.append(privacy) erasures.append(ac)
elif isinstance(device, DataStorage): elif isinstance(device, DataStorage):
if device.privacy: ac = device.last_erase_action
erasures.append(device.privacy) if ac:
erasures.append(ac)
return erasures return erasures
def get_costum_details(self, erasures): def get_costum_details(self, erasures):
@ -1264,9 +1265,14 @@ class ExportsView(View):
erasures_host, erasures_on_server = a, b erasures_host, erasures_on_server = a, b
erasures_host = set(erasures_host) erasures_host = set(erasures_host)
result = 'Success' result_success = 0
if "Failed" in [e.severity.get_public_name() for e in erasures]: result_failed = 0
result = 'Failed' for e in erasures:
result = e.severity.get_public_name()
if "Failed" == result:
result_failed += 1
if "Success" == result:
result_success += 1
erasures = sorted(erasures, key=lambda x: x.end_time) erasures = sorted(erasures, key=lambda x: x.end_time)
erasures_on_server = sorted(erasures_on_server, key=lambda x: x.end_time) erasures_on_server = sorted(erasures_on_server, key=lambda x: x.end_time)
@ -1283,7 +1289,8 @@ class ExportsView(View):
'software': software, 'software': software,
'my_data': my_data, 'my_data': my_data,
'n_computers': n_computers, 'n_computers': n_computers,
'result': result, 'result_success': result_success,
'result_failed': result_failed,
'customer_details': customer_details, 'customer_details': customer_details,
'erasure_hosts': erasures_host, 'erasure_hosts': erasures_host,
'erasures_normal': erasures_normal, 'erasures_normal': erasures_normal,

View file

@ -0,0 +1,45 @@
"""add new erase_data_wipe
Revision ID: 5169765e2653
Revises: 2f2ef041483a
Create Date: 2023-05-23 10:34:46.312074
"""
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '5169765e2653'
down_revision = '2f2ef041483a'
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(
'erase_data_wipe',
sa.Column('document_id', sa.BigInteger(), nullable=False),
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(
['document_id'],
[f'{get_inv()}.document.id'],
),
sa.ForeignKeyConstraint(
['id'],
[f'{get_inv()}.erase_basic.id'],
),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}',
)
def downgrade():
op.drop_table('erase_data_wipe', schema=f'{get_inv()}')

View file

@ -246,6 +246,11 @@ class DataWipeDef(ActionDef):
SCHEMA = schemas.DataWipe SCHEMA = schemas.DataWipe
class EraseDataWipe(ActionDef):
VIEW = None
SCHEMA = schemas.EraseDataWipe
class AllocateDef(ActionDef): class AllocateDef(ActionDef):
VIEW = AllocateView VIEW = AllocateView
SCHEMA = schemas.Allocate SCHEMA = schemas.Allocate

View file

@ -483,6 +483,8 @@ class EraseBasic(JoinedWithOneDeviceMixin, ActionWithOneDevice):
""" """
if self.snapshot: if self.snapshot:
return self.snapshot.device.phid() return self.snapshot.device.phid()
if self.parent:
return self.parent.phid()
return '' return ''
def get_public_name(self): def get_public_name(self):
@ -544,6 +546,24 @@ class ErasePhysical(EraseBasic):
return "Physical" return "Physical"
class EraseDataWipe(EraseBasic):
"""The device has been selected for insert one proof of erease disk."""
id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True)
document_comment = """The user that gets the device due this deal."""
document_id = db.Column(
BigInteger, db.ForeignKey('data_wipe_document.id'), nullable=False
)
document = db.relationship(
'DataWipeDocument',
backref=backref('erase_actions', lazy=True, cascade=CASCADE_OWN),
primaryjoin='EraseDataWipe.document_id == DataWipeDocument.id',
)
def get_public_name(self):
return "EraseDataWipe"
class Step(db.Model): class Step(db.Model):
erasure_id = Column( erasure_id = Column(
UUID(as_uuid=True), UUID(as_uuid=True),
@ -1539,6 +1559,7 @@ class ToPrepare(ActionWithMultipleDevices):
class DataWipe(JoinedTableMixin, ActionWithMultipleDevices): class DataWipe(JoinedTableMixin, ActionWithMultipleDevices):
# class DataWipe(JoinedWithOneDeviceMixin, ActionWithOneDevice):
"""The device has been selected for insert one proof of erease disk.""" """The device has been selected for insert one proof of erease disk."""
document_comment = """The user that gets the device due this deal.""" document_comment = """The user that gets the device due this deal."""

View file

@ -45,8 +45,8 @@ from ereuse_devicehub.resources.tradedocument import schemas as s_document
from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.user import schemas as s_user from ereuse_devicehub.resources.user import schemas as s_user
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.teal.enums import Country, Currency, Subdivision from ereuse_devicehub.teal.enums import Currency
from ereuse_devicehub.teal.marshmallow import IP, URL, EnumField, SanitizedStr, Version from ereuse_devicehub.teal.marshmallow import URL, EnumField, SanitizedStr, Version
from ereuse_devicehub.teal.resource import Schema from ereuse_devicehub.teal.resource import Schema
@ -588,6 +588,11 @@ class DataWipe(ActionWithMultipleDevicesCheckingOwner):
document = NestedOn(s_generic_document.DataWipeDocument, only_query='id') document = NestedOn(s_generic_document.DataWipeDocument, only_query='id')
class EraseDataWipe(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.DataWipe.__doc__
document = NestedOn(s_generic_document.DataWipeDocument, only_query='id')
class Live(ActionWithOneDevice): class Live(ActionWithOneDevice):
__doc__ = m.Live.__doc__ __doc__ = m.Live.__doc__
""" """
@ -808,12 +813,12 @@ class Trade(ActionWithMultipleDevices):
@pre_load @pre_load
def adding_devices(self, data: dict): def adding_devices(self, data: dict):
if not 'devices' in data.keys(): if 'devices' not in data.keys():
data['devices'] = [] data['devices'] = []
@validates_schema @validates_schema
def validate_lot(self, data: dict): def validate_lot(self, data: dict):
if not g.user.email in [data['user_from_email'], data['user_to_email']]: if g.user.email not in [data['user_from_email'], data['user_to_email']]:
txt = "you need to be one of the users of involved in the Trade" txt = "you need to be one of the users of involved in the Trade"
raise ValidationError(txt) raise ValidationError(txt)
@ -879,7 +884,7 @@ class Trade(ActionWithMultipleDevices):
txt = "you need one user for to do a trade" txt = "you need one user for to do a trade"
raise ValidationError(txt) raise ValidationError(txt)
if not g.user.email in [user_from, user_to]: if g.user.email not in [user_from, user_to]:
txt = "you need to be one of participate of the action" txt = "you need to be one of participate of the action"
raise ValidationError(txt) raise ValidationError(txt)

View file

@ -1355,6 +1355,22 @@ class Computer(Device):
if privacy if privacy
) )
@property
def last_erase_action(self):
components = self.components
if self.placeholder and self.placeholder.binding:
components = self.placeholder.binding.components
return set(
ac
for ac in (
hdd.last_erase_action
for hdd in components
if isinstance(hdd, DataStorage)
)
if ac
)
@property @property
def external_document_erasure(self): def external_document_erasure(self):
"""Returns the external ``DataStorage`` proof of erasure.""" """Returns the external ``DataStorage`` proof of erasure."""
@ -1568,12 +1584,40 @@ class DataStorage(JoinedComponentTableMixin, Component):
ev = None ev = None
return ev return ev
@property
def last_erase_action(self):
erase_auto = None
erase_manual = None
if self.binding:
erase_auto = self.privacy
erase_manual = self.binding.device.privacy
if self.placeholder:
erase_manual = self.privacy
if self.placeholder.binding:
erase_auto = self.placeholder.binding.privacy
if erase_auto and erase_manual:
return (
erase_auto
if erase_auto.created > erase_manual.created
else erase_manual
)
if erase_manual:
return erase_manual
if erase_auto:
return erase_auto
return None
def __format__(self, format_spec): def __format__(self, format_spec):
v = super().__format__(format_spec) v = super().__format__(format_spec)
if 's' in format_spec: if 's' in format_spec:
v += ' {} GB'.format(self.size // 1000 if self.size else '?') v += ' {} GB'.format(self.size // 1000 if self.size else '?')
return v return v
def get_size(self):
return '{} GB'.format(self.size // 1000 if self.size else '?')
@property @property
def external_document_erasure(self): def external_document_erasure(self):
"""Returns the external ``DataStorage`` proof of erasure.""" """Returns the external ``DataStorage`` proof of erasure."""

View file

@ -209,7 +209,7 @@
</a> </a>
</li> </li>
<li> <li>
<a href="javascript:newDataWipe('DataWipe')" class="dropdown-item"> <a href="javascript:newDataWipe('EraseDataWipe')" class="dropdown-item">
<i class="bi bi-eraser-fill"></i> <i class="bi bi-eraser-fill"></i>
DataWipe DataWipe
</a> </a>

View file

@ -66,91 +66,108 @@
</style> </style>
</head> </head>
<body> <body>
<header class="page-header"> <header class="page-header">
<div class="col" style="background-color: #d5a6bd;">
<p style="margin-left: 10px;">{{ date_report }}, {{ software }}</p>
</div>
</header>
<div class="container">
<div class="row"> <div class="row">
<div class="col-6">
<img class="company-logo" src="{{ customer_details and customer_details.logo.to_text() or '' }}" />
</div>
<div class="col-6">
<img class="customer-logo" src="{{ my_data and my_data.logo.to_text() }}" />
</div>
</div>
</div>
<div class="container body-content">
<div class="row mt-3">
<div class="col"> <div class="col">
<h1>Data Sanitization Certificate</h1> <h1>Data Sanitization Certificate</h1>
</div> </div>
</div> <div class="col" style="background-color: #d5a6bd;">
<p style="margin-left: 10px;">{{ date_report }}, {{ software }}</p>
<div class="row mt-3">
<div class="col-12">
<strong>Entity Information</strong>
</div> </div>
</div>
</header>
<div class="container body-content">
<div class="row mt-3">
<div class="col-12"> <div class="col-12">
<table class="body_content"> <table class="body_content">
<tbody> <tbody>
<tr style="padding-top:5px;"> <tr>
<td style="width:20%;">
Name:
</td>
<td style="width:80%;"> <td style="width:80%;">
<span>{{ customer_details and customer_details.company_name or ''}}</span> <table class="body_content">
</td> <tbody>
</tr> <tr style="padding-top:5px;">
<tr style="padding-top:5px;"> <td colspan="2">
<td style="width:20%;"> <strong>Responsible Sanitization Entity</strong>
Location: </td>
</td> </tr>
<td style="width:80%;"> <tr style="padding-top:5px;">
<span>{{ customer_details and customer_details.location or '' }}</span> <td style="width:20%;">
<span>Name:</span>
</td>
<td style="width:80%;">
<span>{{ my_data and my_data.company_name or '' }}</span>
</td>
</tr>
<tr style="padding-top:5px;">
<td style="width:20%;">
<span>Responsible Person</span>
</td>
<td style="width:80%;">
<span>{{ my_data and my_data.responsable_person or '' }}</span>
</td>
</tr>
<tr style="padding-top:5px;">
<td style="width:20%;">
<span>Location:</span>
</td>
<td style="width:80%;">
<span>{{ my_data and my_data.location or '' }}</span>
</td>
</tr>
</table>
</td>
<td style="width:20%;">
<img style="width: 100%; height: auto;" src="{{ my_data and my_data.logo.to_text() }}" />
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="row" style="padding-top: 20px;"> <div class="row" style="padding-top: 20px;">
<div class="col-12">
<strong>Responsible Sanitization Entity</strong>
</div>
<div class="col-12"> <div class="col-12">
<table class="body_content"> <table class="body_content">
<tbody> <tbody>
<tr style="padding-top:5px;"> <tr>
<td style="width:20%;">
<span>Name:</span>
</td>
<td style="width:80%;"> <td style="width:80%;">
<span>{{ my_data and my_data.company_name or '' }}</span> <table class="body_content">
</td> <tbody>
</tr> <tr style="padding-top:5px;">
<tr style="padding-top:5px;"> <td colspan="2">
<td style="width:20%;"> <strong>Entity Information</strong>
<span>Responsible Person</span> </td>
</td> </tr>
<td style="width:80%;"> <tr>
<span>{{ my_data and my_data.responsable_person or '' }}</span> <td style="width:20%;">
</td> <span>Name: </span>
</tr> </td>
<tr style="padding-top:5px;"> <td style="width:80%;">
<td style="width:20%;"> <span>{{ customer_details and customer_details.company_name or ''}}</span>
<span>Location:</span> </td>
</td> </tr>
<td style="width:80%;"> <tr>
<span>{{ my_data and my_data.location or '' }}</span> <td style="width:20%;">
<span>Location: </span>
</td>
<td style="width:80%;">
<span>{{ customer_details and customer_details.location or '' }}</span>
</td>
</tr>
</tbody>
</table>
</td>
<td style="width:20%;">
<img style="width: 100%; height: auto;" src="{{ customer_details and customer_details.logo.to_text() or '' }}" />
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="row" style="padding-top: 20px;"> <div class="row" style="padding-top: 20px;">
<div class="col-12"> <div class="col-12">
<strong>Summary</strong> <strong>Summary</strong>
@ -158,6 +175,16 @@
<div class="col-12"> <div class="col-12">
<table class="body_content"> <table class="body_content">
<tbody> <tbody>
{% if customer_details and customer_details.transfer %}
<tr style="padding-top:5px;">
<td style="width:20%;">
<span>Code Transfer:</span>
</td>
<td style="width:80%;">
<span>{{ customer_details.transfer.code or '' }}</span>
</td>
</tr>
{% endif %}
{% if erasure_hosts %} {% if erasure_hosts %}
<tr style="padding-top:5px;"> <tr style="padding-top:5px;">
<td style="width:20%;"> <td style="width:20%;">
@ -166,7 +193,7 @@
<td style="width:80%;"> <td style="width:80%;">
{% for e in erasure_hosts %} {% for e in erasure_hosts %}
{% if e.serial_number %} {% if e.serial_number %}
<span>{{ e.serial_number.upper() }}</span>{% if not loop.last %},{% endif %} <span>{{ (e.serial_number or '').upper() }}</span>{% if not loop.last %},{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</td> </td>
@ -192,10 +219,18 @@
</tr> </tr>
<tr style="padding-top:5px;"> <tr style="padding-top:5px;">
<td style="width:20%;"> <td style="width:20%;">
<span>Sanitization result:</span> <span>N&deg; result Success:</span>
</td> </td>
<td style="width:80%;"> <td style="width:80%;">
<span>{{ result }}</span> <span>{{ result_success }}</span>
</td>
</tr>
<tr style="padding-top:5px;">
<td style="width:20%;">
<span>N&deg; result Failed:</span>
</td>
<td style="width:80%;">
<span>{{ result_failed }}</span>
</td> </td>
</tr> </tr>
</table> </table>
@ -288,10 +323,10 @@
{% for erasure in erasures %} {% for erasure in erasures %}
<tr style="border-bottom: 1px dashed #000;"> <tr style="border-bottom: 1px dashed #000;">
<td> <td>
{{ erasure.device.serial_number and erasure.device.serial_number.upper() or '' }} {{ (erasure.device.serial_number or '').upper() }}
</td> </td>
<td> <td>
{{ erasure.parent.serial_number and erasure.parent.serial_number.upper() or '' }} {{ (erasure.parent.serial_number or '').upper() }}
</td> </td>
<td> <td>
{{ erasure.get_public_name() }} {{ erasure.get_public_name() }}
@ -311,39 +346,43 @@
</div> </div>
{% for erasure in erasures %} {% for erasure in erasures %}
<div class="container mb-5 page-break"> <div class="container mb-5 page-break">
<h4>{{ erasure.device.serial_number.upper() }}</h4> {% if loop.index == 1 %}
<dl> <div class="col-12" style="margin-bottom: 20px;">
<dt>Data storage:</dt> <h3>Tenchnical Details</h3>
<dd>{{ erasure.device.__format__('ts') }}</dd> </div>
{% if erasure.parent %}
<dt>Computer where was erase:</dt>
<dd>Title: {{ erasure.parent.__format__('ts') }}</dd>
<dd>DevicehubID: {{ erasure.parent.dhid }}</dd>
<dd>Hid: {{ erasure.parent.chid }}</dd>
<dd>Tags: {{ erasure.parent.tags }}</dd>
{% if erasure.device.parent %}
<dt>Computer where it resides:</dt>
<dd>Title: {{ erasure.device.parent.__format__('ts') }}</dd>
<dd>DevicehubID: {{ erasure.device.parent.dhid }}</dd>
<dd>Hid: {{ erasure.device.parent.chid }}</dd>
<dd>Tags: {{ erasure.device.parent.tags }}</dd>
{% endif %}
{% endif %} {% endif %}
<h4>{{ (erasure.device.serial_number or '').upper() }}</h4>
<dl>
<dt>Storage Drive:</dt>
<dd>Model: {{ erasure.device.model }}</dd>
<dd>SN: {{ (erasure.device.serial_number or '').upper() }}</dd>
<dd>Size: {{ erasure.device.get_size()}}</dd>
{% if erasure.parent %}
<br />
<dt>Computer Host:</dt>
<dd>Model: {{ erasure.parent.model }}</dd>
<dd>SN: {{ (erasure.parent.serial_number or '').upper() }}</dd>
<dd>DHID: {{ erasure.parent.dhid }}</dd>
{% endif %}
<br />
<dt>Erasure:</dt> <dt>Erasure:</dt>
<dd>{{ erasure.__format__('ts') }}</dd> <dd>{{ erasure.__format__('ts') }}</dd>
{% if erasure.steps %} {% if erasure.steps %}
<dt>Erasure steps:</dt> <dt>Erasure steps:</dt>
<dd> <dd>
<ol> <ol>
{% for step in erasure.steps %} {% for step in erasure.steps %}
<li>{{ step.__format__('') }}</li> <li>{{ step.__format__('') }}</li>
{% endfor %} {% endfor %}
</ol> </ol>
</dd> </dd>
{% endif %} {% endif %}
{% if erasure.type == 'EraseDataWipe' %}
<br />
<dt>Software:</dt>
<dd>{{ erasure.document and erasure.document.software or ''}}</dd>
{% endif %}
{% if erasure.device.proofs %} {% if erasure.device.proofs %}
<dt>DLT Proofs:</dt> <dt>DLT Proofs:</dt>
<dd> <dd>

View file

@ -125,4 +125,4 @@ def test_api_docs(client: Client):
'scheme': 'basic', 'scheme': 'basic',
'name': 'Authorization', 'name': 'Authorization',
} }
assert len(docs['definitions']) == 134 assert len(docs['definitions']) == 135

View file

@ -1375,6 +1375,34 @@ def test_action_datawipe(user3: UserClientFlask):
assert dev.binding.device.devicehub_id in body assert dev.binding.device.devicehub_id in body
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_action_erasedatawipe(user3: UserClientFlask):
snap = create_device(user3, 'real-eee-1001pxd.snapshot.12.json')
dev = snap.device
uri = '/inventory/device/'
user3.get(uri)
b_file = b'1234567890'
file_name = "my_file.doc"
file_upload = (BytesIO(b_file), file_name)
data = {
'csrf_token': generate_csrf(),
'type': "EraseDataWipe",
'severity': "Info",
'devices': "{}".format(dev.binding.device.id),
'document-file_name': file_upload,
}
uri = '/inventory/action/datawipe/add/'
body, status = user3.post(uri, data=data, content_type="multipart/form-data")
assert status == '200 OK'
assert dev.binding.device.actions[-1].type == 'EraseDataWipe'
assert 'Action &#34;EraseDataWipe&#34; created successfully!' in body
assert dev.binding.device.devicehub_id in body
@pytest.mark.mvp @pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__) @pytest.mark.usefixtures(conftest.app_context.__name__)
def test_wb_settings(user3: UserClientFlask): def test_wb_settings(user3: UserClientFlask):