Merge pull request #204 from eReuse/feature/server-side-render-exports
Feature/server side render exports
This commit is contained in:
commit
1e9b65e0b5
|
@ -1,7 +1,12 @@
|
|||
import csv
|
||||
from io import StringIO
|
||||
|
||||
import flask
|
||||
from flask import Blueprint, g, request, url_for
|
||||
import flask_weasyprint
|
||||
from flask import Blueprint, g, make_response, request, url_for
|
||||
from flask.views import View
|
||||
from flask_login import current_user, login_required
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from ereuse_devicehub import messages
|
||||
from ereuse_devicehub.inventory.forms import (
|
||||
|
@ -18,7 +23,10 @@ from ereuse_devicehub.inventory.forms import (
|
|||
TradeForm,
|
||||
UploadSnapshotForm,
|
||||
)
|
||||
from ereuse_devicehub.resources.device.models import Device
|
||||
from ereuse_devicehub.resources.action.models import Trade
|
||||
from ereuse_devicehub.resources.device.models import Computer, DataStorage, Device
|
||||
from ereuse_devicehub.resources.documents.device_row import ActionRow, DeviceRow
|
||||
from ereuse_devicehub.resources.hash_reports import insert_hash
|
||||
from ereuse_devicehub.resources.lot.models import Lot
|
||||
from ereuse_devicehub.resources.tag.model import Tag
|
||||
|
||||
|
@ -38,7 +46,7 @@ class DeviceListMix(View):
|
|||
lot = None
|
||||
tags = (
|
||||
Tag.query.filter(Tag.owner_id == current_user.id)
|
||||
.filter(Tag.device_id == None)
|
||||
.filter(Tag.device_id.is_(None))
|
||||
.order_by(Tag.created.desc())
|
||||
)
|
||||
|
||||
|
@ -58,7 +66,7 @@ class DeviceListMix(View):
|
|||
devices = (
|
||||
Device.query.filter(Device.owner_id == current_user.id)
|
||||
.filter(Device.type.in_(filter_types))
|
||||
.filter(Device.lots == None)
|
||||
.filter_by(lots=None)
|
||||
.order_by(Device.updated.desc())
|
||||
)
|
||||
form_new_action = NewActionForm()
|
||||
|
@ -330,7 +338,7 @@ class NewActionView(View):
|
|||
self.form = self.form_class()
|
||||
|
||||
if self.form.validate_on_submit():
|
||||
instance = self.form.save()
|
||||
self.form.save()
|
||||
messages.success(
|
||||
'Action "{}" created successfully!'.format(self.form.type.data)
|
||||
)
|
||||
|
@ -355,7 +363,7 @@ class NewAllocateView(NewActionView, DeviceListMix):
|
|||
self.form = self.form_class()
|
||||
|
||||
if self.form.validate_on_submit():
|
||||
instance = self.form.save()
|
||||
self.form.save()
|
||||
messages.success(
|
||||
'Action "{}" created successfully!'.format(self.form.type.data)
|
||||
)
|
||||
|
@ -377,7 +385,7 @@ class NewDataWipeView(NewActionView, DeviceListMix):
|
|||
self.form = self.form_class()
|
||||
|
||||
if self.form.validate_on_submit():
|
||||
instance = self.form.save()
|
||||
self.form.save()
|
||||
messages.success(
|
||||
'Action "{}" created successfully!'.format(self.form.type.data)
|
||||
)
|
||||
|
@ -399,7 +407,7 @@ class NewTradeView(NewActionView, DeviceListMix):
|
|||
self.form = self.form_class()
|
||||
|
||||
if self.form.validate_on_submit():
|
||||
instance = self.form.save()
|
||||
self.form.save()
|
||||
messages.success(
|
||||
'Action "{}" created successfully!'.format(self.form.type.data)
|
||||
)
|
||||
|
@ -434,6 +442,131 @@ class NewTradeDocumentView(View):
|
|||
)
|
||||
|
||||
|
||||
class ExportsView(View):
|
||||
methods = ['GET']
|
||||
decorators = [login_required]
|
||||
|
||||
def dispatch_request(self, export_id):
|
||||
export_ids = {
|
||||
'metrics': self.metrics,
|
||||
'devices': self.devices_list,
|
||||
'certificates': self.erasure,
|
||||
'links': self.public_links,
|
||||
}
|
||||
|
||||
if export_id not in export_ids:
|
||||
return NotFound()
|
||||
return export_ids[export_id]()
|
||||
|
||||
def find_devices(self):
|
||||
args = request.args.get('ids')
|
||||
ids = args.split(',') if args else []
|
||||
query = Device.query.filter(Device.owner == g.user)
|
||||
return query.filter(Device.devicehub_id.in_(ids))
|
||||
|
||||
def response_csv(self, data, name):
|
||||
bfile = data.getvalue().encode('utf-8')
|
||||
# insert proof
|
||||
insert_hash(bfile)
|
||||
output = make_response(bfile)
|
||||
output.headers['Content-Disposition'] = 'attachment; filename={}'.format(name)
|
||||
output.headers['Content-type'] = 'text/csv'
|
||||
return output
|
||||
|
||||
def devices_list(self):
|
||||
"""Get device query and put information in csv format."""
|
||||
data = StringIO()
|
||||
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
|
||||
first = True
|
||||
|
||||
for device in self.find_devices():
|
||||
d = DeviceRow(device, {})
|
||||
if first:
|
||||
cw.writerow(d.keys())
|
||||
first = False
|
||||
cw.writerow(d.values())
|
||||
|
||||
return self.response_csv(data, "export.csv")
|
||||
|
||||
def metrics(self):
|
||||
"""Get device query and put information in csv format."""
|
||||
data = StringIO()
|
||||
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
|
||||
first = True
|
||||
devs_id = []
|
||||
# Get the allocate info
|
||||
for device in self.find_devices():
|
||||
devs_id.append(device.id)
|
||||
for allocate in device.get_metrics():
|
||||
d = ActionRow(allocate)
|
||||
if first:
|
||||
cw.writerow(d.keys())
|
||||
first = False
|
||||
cw.writerow(d.values())
|
||||
|
||||
# Get the trade info
|
||||
query_trade = Trade.query.filter(
|
||||
Trade.devices.any(Device.id.in_(devs_id))
|
||||
).all()
|
||||
|
||||
lot_id = request.args.get('lot')
|
||||
if lot_id and not query_trade:
|
||||
lot = Lot.query.filter_by(id=lot_id).one()
|
||||
if hasattr(lot, "trade") and lot.trade:
|
||||
if g.user in [lot.trade.user_from, lot.trade.user_to]:
|
||||
query_trade = [lot.trade]
|
||||
|
||||
for trade in query_trade:
|
||||
data_rows = trade.get_metrics()
|
||||
for row in data_rows:
|
||||
d = ActionRow(row)
|
||||
if first:
|
||||
cw.writerow(d.keys())
|
||||
first = False
|
||||
cw.writerow(d.values())
|
||||
|
||||
return self.response_csv(data, "actions_export.csv")
|
||||
|
||||
def public_links(self):
|
||||
# get a csv with the publink links of this devices
|
||||
data = StringIO()
|
||||
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
|
||||
cw.writerow(['links'])
|
||||
host_url = request.host_url
|
||||
for dev in self.find_devices():
|
||||
code = dev.devicehub_id
|
||||
link = [f"{host_url}devices/{code}"]
|
||||
cw.writerow(link)
|
||||
|
||||
return self.response_csv(data, "links.csv")
|
||||
|
||||
def erasure(self):
|
||||
template = self.build_erasure_certificate()
|
||||
res = flask_weasyprint.render_pdf(
|
||||
flask_weasyprint.HTML(string=template),
|
||||
download_filename='erasure-certificate.pdf',
|
||||
)
|
||||
insert_hash(res.data)
|
||||
return res
|
||||
|
||||
def build_erasure_certificate(self):
|
||||
erasures = []
|
||||
for device in self.find_devices():
|
||||
if isinstance(device, Computer):
|
||||
for privacy in device.privacy:
|
||||
erasures.append(privacy)
|
||||
elif isinstance(device, DataStorage):
|
||||
if device.privacy:
|
||||
erasures.append(device.privacy)
|
||||
|
||||
params = {
|
||||
'title': 'Erasure Certificate',
|
||||
'erasures': tuple(erasures),
|
||||
'url_pdf': '',
|
||||
}
|
||||
return flask.render_template('inventory/erasure.html', **params)
|
||||
|
||||
|
||||
devices.add_url_rule('/action/add/', view_func=NewActionView.as_view('action_add'))
|
||||
devices.add_url_rule('/action/trade/add/', view_func=NewTradeView.as_view('trade_add'))
|
||||
devices.add_url_rule(
|
||||
|
@ -483,3 +616,6 @@ devices.add_url_rule(
|
|||
'/tag/devices/<int:id>/del/',
|
||||
view_func=TagUnlinkDeviceView.as_view('tag_devices_del'),
|
||||
)
|
||||
devices.add_url_rule(
|
||||
'/export/<string:export_id>/', view_func=ExportsView.as_view('export')
|
||||
)
|
||||
|
|
|
@ -148,3 +148,14 @@ function get_device_list() {
|
|||
description = $.map(list_devices, function(x) { return x }).join(", ");
|
||||
$(".enumeration-devices").html(description);
|
||||
}
|
||||
|
||||
function export_file(type_file) {
|
||||
var devices = $(".deviceSelect").filter(':checked');
|
||||
var devices_id = $.map(devices, function(x) { return $(x).attr('data-device-dhid')}).join(",");
|
||||
if (devices_id){
|
||||
var url = "/inventory/export/"+type_file+"/?ids="+devices_id;
|
||||
window.location.href = url;
|
||||
} else {
|
||||
$("#exportAlertModal").click();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<div class="modal fade" id="exportErrorModal" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Error export</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p class="text-danger pol">
|
||||
You need select first some device for use export file
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -205,10 +205,30 @@
|
|||
<i class="bi bi-reply"></i>
|
||||
Exports
|
||||
</button>
|
||||
<span class="d-none" id="exportAlertModal" data-bs-toggle="modal" data-bs-target="#exportErrorModal"></span>
|
||||
<ul class="dropdown-menu" aria-labelledby="btnExport">
|
||||
<li>
|
||||
<a href="#" class="dropdown-item">
|
||||
TODO: Not implemented
|
||||
<a href="javascript:export_file('devices')" class="dropdown-item">
|
||||
<i class="bi bi-file-spreadsheet"></i>
|
||||
Devices Spreadsheet
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:export_file('metrics')" class="dropdown-item">
|
||||
<i class="bi bi-file-spreadsheet"></i>
|
||||
Metrics Spreadsheet
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:export_file('links')" class="dropdown-item">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
Public Links
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="javascript:export_file('certificates')" class="dropdown-item">
|
||||
<i class="bi bi-eraser-fill"></i>
|
||||
Erasure Certificate
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -353,6 +373,7 @@
|
|||
{% include "inventory/allocate.html" %}
|
||||
{% include "inventory/data_wipe.html" %}
|
||||
{% include "inventory/trade.html" %}
|
||||
{% include "inventory/alert_export_error.html" %}
|
||||
|
||||
<!-- CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@latest"></script>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
{% extends "documents/layout.html" %}
|
||||
{% block body %}
|
||||
<div>
|
||||
<h2>Summary</h2>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>S/N Data Storage</th>
|
||||
<th>Type of erasure</th>
|
||||
<th>Result</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for erasure in erasures %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ erasure.device.serial_number.upper() }}
|
||||
</td>
|
||||
<td>
|
||||
{{ erasure.type }}
|
||||
</td>
|
||||
<td>
|
||||
{{ erasure.severity }}
|
||||
</td>
|
||||
<td>
|
||||
{{ erasure.date_str }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="page-break row">
|
||||
<h2>Details</h2>
|
||||
{% for erasure in erasures %}
|
||||
<div class="col-md-6 no-page-break">
|
||||
<h4>{{ erasure.device.__format__('t') }}</h4>
|
||||
<dl>
|
||||
<dt>Data storage:</dt>
|
||||
<dd>{{ erasure.device.__format__('ts') }}</dd>
|
||||
|
||||
<dt>Computer where was erase:</dt>
|
||||
<dd>Title: {{ erasure.parent.__format__('ts') }}</dd>
|
||||
<dd>DevicehubID: {{ erasure.parent.devicehub_id }}</dd>
|
||||
<dd>Hid: {{ erasure.parent.hid }}</dd>
|
||||
<dd>Tags: {{ erasure.parent.tags }}</dd>
|
||||
|
||||
<dt>Computer where it resides:</dt>
|
||||
<dd>Title: {{ erasure.device.parent.__format__('ts') }}</dd>
|
||||
<dd>DevicehubID: {{ erasure.device.parent.devicehub_id }}</dd>
|
||||
<dd>Hid: {{ erasure.device.parent.hid }}</dd>
|
||||
<dd>Tags: {{ erasure.device.parent.tags }}</dd>
|
||||
|
||||
<dt>Erasure:</dt>
|
||||
<dd>{{ erasure.__format__('ts') }}</dd>
|
||||
{% if erasure.steps %}
|
||||
<dt>Erasure steps:</dt>
|
||||
<dd>
|
||||
<ol>
|
||||
{% for step in erasure.steps %}
|
||||
<li>{{ step.__format__('') }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="no-page-break">
|
||||
<h2>Glossary</h2>
|
||||
<dl>
|
||||
<dt>Erase Basic</dt>
|
||||
<dd>
|
||||
A software-based fast non-100%-secured way of erasing data storage,
|
||||
using <a href="https://en.wikipedia.org/wiki/Shred_(Unix)">shred</a>.
|
||||
</dd>
|
||||
<dt>Erase Sectors</dt>
|
||||
<dd>
|
||||
A secured-way of erasing data storages, checking sector-by-sector
|
||||
the erasure, using <a href="https://en.wikipedia.org/wiki/Badblocks">badblocks</a>.
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="no-print">
|
||||
<a href="{{ url_pdf }}">Click here to download the PDF.</a>
|
||||
</div>
|
||||
<div class="print-only">
|
||||
<a href="{{ url_for('Document.StampsView', _external=True) }}">Verify on-line the integrity of this document</a>
|
||||
</div>
|
||||
{% endblock %}
|
Reference in New Issue