Merge pull request #396 from eReuse/changes/3853-report

Changes/3853 report
This commit is contained in:
cayop 2022-11-03 23:56:57 +01:00 committed by GitHub
commit c784fb7499
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 114 additions and 293 deletions

5
.gitignore vendored
View file

@ -127,3 +127,8 @@ yarn.lock
# ESLint Report # ESLint Report
eslint_report.json eslint_report.json
modules/
tmp/
.env*
bin/
env*

View file

@ -1,96 +0,0 @@
import csv
# import click_spinner
# import ereuse_utils.cli
from io import StringIO
from ereuse_devicehub.resources.action import models as evs
from ereuse_devicehub.resources.device.models import Placeholder
from ereuse_devicehub.resources.documents.device_row import InternalStatsRow
# import click
class Report:
def __init__(self, app) -> None:
super().__init__()
self.app = app
short_help = 'Creates reports devices and users.'
self.app.cli.command('report', short_help=short_help)(self.run)
def run(self):
stats = InternalStatsView()
stats.print()
class InternalStatsView:
def print(self):
query = evs.Action.query.filter(
evs.Action.type.in_(
(
'Snapshot',
'Live',
'Allocate',
'Deallocate',
'EraseBasic',
'EraseSectors',
)
)
)
return self.generate_post_csv(query)
def generate_post_csv(self, query):
data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
cw.writerow(InternalStatsRow('', "2000-1", [], []).keys())
for row in self.get_rows(query):
cw.writerow(row)
return print(data.getvalue())
def get_rows(self, query):
d = {}
dd = {}
disks = []
for ac in query:
create = '{}-{}'.format(ac.created.year, ac.created.month)
user = ac.author.email
if user not in d:
d[user] = {}
dd[user] = {}
if create not in d[user]:
d[user][create] = []
dd[user][create] = None
d[user][create].append(ac)
for user, createds in d.items():
for create, actions in createds.items():
r = InternalStatsRow(user, create, actions, disks)
dd[user][create] = r
return self.get_placeholders(dd)
def get_placeholders(self, dd):
for p in Placeholder.query.all():
create = '{}-{}'.format(p.created.year, p.created.month)
user = p.owner.email
if user not in dd:
dd[user] = {}
if create not in dd[user]:
dd[user][create] = None
if not dd[user][create]:
dd[user][create] = InternalStatsRow(user, create, [], [])
dd[user][create]['Placeholders'] += 1
rows = []
for user, createds in dd.items():
for create, row in createds.items():
rows.append(row.values())
return rows

View file

@ -15,7 +15,8 @@ from teal.teal import Teal
from ereuse_devicehub.auth import Auth from ereuse_devicehub.auth import Auth
from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.client import Client, UserClient
from ereuse_devicehub.commands.reports import Report
# from ereuse_devicehub.commands.reports import Report
from ereuse_devicehub.commands.users import GetToken from ereuse_devicehub.commands.users import GetToken
from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
@ -29,7 +30,7 @@ from ereuse_devicehub.templating import Environment
class Devicehub(Teal): class Devicehub(Teal):
test_client_class = Client test_client_class = Client
Dummy = Dummy Dummy = Dummy
Report = Report # Report = Report
jinja_environment = Environment jinja_environment = Environment
def __init__( def __init__(
@ -70,7 +71,7 @@ class Devicehub(Teal):
self.id = inventory self.id = inventory
"""The Inventory ID of this instance. In Teal is the app.schema.""" """The Inventory ID of this instance. In Teal is the app.schema."""
self.dummy = Dummy(self) self.dummy = Dummy(self)
self.report = Report(self) # self.report = Report(self)
self.get_token = GetToken(self) self.get_token = GetToken(self)
@self.cli.group( @self.cli.group(

View file

@ -614,104 +614,3 @@ class ActionRow(OrderedDict):
self['Type'] = allocate['type'] self['Type'] = allocate['type']
self['LiveCreate'] = allocate['liveCreate'] self['LiveCreate'] = allocate['liveCreate']
self['UsageTimeHdd'] = allocate['usageTimeHdd'] self['UsageTimeHdd'] = allocate['usageTimeHdd']
class InternalStatsRow(OrderedDict):
def __init__(self, user, create, actions, disks):
super().__init__()
# General information about all internal stats
# user, quart, month, year:
# Snapshot (Registers)
# Snapshots (Update)
# Snapshots (All)
# Drives Erasure
# Drives Erasure Uniques
# Placeholders
# Allocate
# Deallocate
# Live
self.actions = actions
year, month = create.split('-')
self.disks = disks
self['User'] = user
self['Year'] = year
self['Quarter'] = self.quarter(month)
self['Month'] = month
self['Snapshot (Registers)'] = 0
self['Snapshot (Update)'] = 0
self['Snapshot (All)'] = 0
self['Drives Erasure'] = 0
self['Drives Erasure Uniques'] = 0
self['Placeholders'] = 0
self['Allocates'] = 0
self['Deallocates'] = 0
self['Lives'] = 0
self.count_actions()
def count_actions(self):
for ac in self.actions:
self.is_snapshot(
self.is_deallocate(self.is_live(self.is_allocate(self.is_erase(ac))))
)
def is_allocate(self, ac):
if ac.type == 'Allocate':
self['Allocates'] += 1
return ac
def is_erase(self, ac):
if ac.type in ['EraseBasic', 'EraseSectors']:
self['Drives Erasure'] += 1
if ac.device in self.disks:
return ac
self['Drives Erasure Uniques'] += 1
self.disks.append(ac.device)
return ac
def is_live(self, ac):
if ac.type == 'Live':
self['Lives'] += 1
return ac
def is_deallocate(self, ac):
if ac.type == 'Deallocate':
self['Deallocates'] += 1
return ac
def is_snapshot(self, ac):
if not ac.type == 'Snapshot':
return
self['Snapshot (All)'] += 1
canary = False
for _ac in ac.device.actions:
if not _ac.type == 'Snapshot':
continue
if _ac.created < ac.created:
canary = True
break
if canary:
self['Snapshot (Update)'] += 1
else:
self['Snapshot (Registers)'] += 1
def quarter(self, month):
q = {
1: 'Q1',
2: 'Q1',
3: 'Q1',
4: 'Q2',
5: 'Q2',
6: 'Q2',
7: 'Q3',
8: 'Q3',
9: 'Q3',
10: 'Q4',
11: 'Q4',
12: 'Q4',
}
return q[int(month)]

View file

@ -30,7 +30,6 @@ from ereuse_devicehub.resources.device.views import DeviceView
from ereuse_devicehub.resources.documents.device_row import ( from ereuse_devicehub.resources.documents.device_row import (
ActionRow, ActionRow,
DeviceRow, DeviceRow,
InternalStatsRow,
StockRow, StockRow,
) )
from ereuse_devicehub.resources.enums import SessionType from ereuse_devicehub.resources.enums import SessionType
@ -62,9 +61,9 @@ class DocumentView(DeviceView):
200: 200:
description: Return the collection or the specific one. description: Return the collection or the specific one.
""" """
args = self.QUERY_PARSER.parse(self.find_args, args = self.QUERY_PARSER.parse(
flask.request, self.find_args, flask.request, locations=('querystring',)
locations=('querystring',)) )
ids = [] ids = []
if 'filter' in request.args: if 'filter' in request.args:
filters = json.loads(request.args.get('filter', {})) filters = json.loads(request.args.get('filter', {}))
@ -113,7 +112,8 @@ class DocumentView(DeviceView):
template = self.erasure(query) template = self.erasure(query)
if args.get('format') == Format.PDF: if args.get('format') == Format.PDF:
res = flask_weasyprint.render_pdf( res = flask_weasyprint.render_pdf(
flask_weasyprint.HTML(string=template), download_filename='{}.pdf'.format(type) flask_weasyprint.HTML(string=template),
download_filename='{}.pdf'.format(type),
) )
insert_hash(res.data) insert_hash(res.data)
else: else:
@ -140,7 +140,7 @@ class DocumentView(DeviceView):
params = { params = {
'title': 'Erasure Certificate', 'title': 'Erasure Certificate',
'erasures': tuple(erasures()), 'erasures': tuple(erasures()),
'url_pdf': url_pdf.to_text() 'url_pdf': url_pdf.to_text(),
} }
return flask.render_template('documents/erasure.html', **params) return flask.render_template('documents/erasure.html', **params)
@ -159,7 +159,13 @@ class DevicesDocumentView(DeviceView):
def generate_post_csv(self, query): def generate_post_csv(self, query):
"""Get device query and put information in csv format.""" """Get device query and put information in csv format."""
data = StringIO() data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"', quoting=csv.QUOTE_ALL) cw = csv.writer(
data,
delimiter=';',
lineterminator="\n",
quotechar='"',
quoting=csv.QUOTE_ALL,
)
first = True first = True
document_ids = self.get_documents_id() document_ids = self.get_documents_id()
for device in query: for device in query:
@ -193,7 +199,13 @@ class ActionsDocumentView(DeviceView):
def generate_post_csv(self, query): def generate_post_csv(self, query):
"""Get device query and put information in csv format.""" """Get device query and put information in csv format."""
data = StringIO() data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"', quoting=csv.QUOTE_ALL) cw = csv.writer(
data,
delimiter=';',
lineterminator="\n",
quotechar='"',
quoting=csv.QUOTE_ALL,
)
first = True first = True
devs_id = [] devs_id = []
for device in query: for device in query:
@ -204,7 +216,9 @@ class ActionsDocumentView(DeviceView):
cw.writerow(d.keys()) cw.writerow(d.keys())
first = False first = False
cw.writerow(d.values()) cw.writerow(d.values())
query_trade = Trade.query.filter(Trade.devices.any(Device.id.in_(devs_id))).all() query_trade = Trade.query.filter(
Trade.devices.any(Device.id.in_(devs_id))
).all()
lot_id = request.args.get('lot') lot_id = request.args.get('lot')
if lot_id and not query_trade: if lot_id and not query_trade:
@ -225,7 +239,9 @@ class ActionsDocumentView(DeviceView):
bfile = data.getvalue().encode('utf-8') bfile = data.getvalue().encode('utf-8')
output = make_response(bfile) output = make_response(bfile)
insert_hash(bfile) insert_hash(bfile)
output.headers['Content-Disposition'] = 'attachment; filename=actions_export.csv' output.headers[
'Content-Disposition'
] = 'attachment; filename=actions_export.csv'
output.headers['Content-type'] = 'text/csv' output.headers['Content-type'] = 'text/csv'
return output return output
@ -277,7 +293,13 @@ class StockDocumentView(DeviceView):
def generate_post_csv(self, query): def generate_post_csv(self, query):
"""Get device query and put information in csv format.""" """Get device query and put information in csv format."""
data = StringIO() data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"', quoting=csv.QUOTE_ALL) cw = csv.writer(
data,
delimiter=';',
lineterminator="\n",
quotechar='"',
quoting=csv.QUOTE_ALL,
)
first = True first = True
for device in query: for device in query:
d = StockRow(device) d = StockRow(device)
@ -311,6 +333,7 @@ class StampsView(View):
This view render one public ans static page for see the links for to do the check This view render one public ans static page for see the links for to do the check
of one csv file of one csv file
""" """
def get_url_path(self): def get_url_path(self):
url = urlutils.URL(request.url) url = urlutils.URL(request.url)
url.normalize() url.normalize()
@ -319,8 +342,9 @@ class StampsView(View):
def get(self): def get(self):
result = ('', '') result = ('', '')
return flask.render_template('documents/stamp.html', rq_url=self.get_url_path(), return flask.render_template(
result=result) 'documents/stamp.html', rq_url=self.get_url_path(), result=result
)
def post(self): def post(self):
result = ('', '') result = ('', '')
@ -331,54 +355,26 @@ class StampsView(View):
ok = '100% coincidence. The attached file contains data 100% existing in \ ok = '100% coincidence. The attached file contains data 100% existing in \
to our backend' to our backend'
result = ('Bad', bad) result = ('Bad', bad)
mime = ['text/csv', 'application/pdf', 'text/plain', 'text/markdown', mime = [
'image/jpeg', 'image/png', 'text/html', 'text/csv',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/pdf',
'application/vnd.oasis.opendocument.spreadsheet', 'text/plain',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/markdown',
'application/msword'] 'image/jpeg',
'image/png',
'text/html',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/msword',
]
if file_check.mimetype in mime: if file_check.mimetype in mime:
if verify_hash(file_check): if verify_hash(file_check):
result = ('Ok', ok) result = ('Ok', ok)
return flask.render_template('documents/stamp.html', rq_url=self.get_url_path(), return flask.render_template(
result=result) 'documents/stamp.html', rq_url=self.get_url_path(), result=result
)
class InternalStatsView(DeviceView):
@cache(datetime.timedelta(minutes=1))
def find(self, args: dict):
if not g.user.email == app.config['EMAIL_ADMIN']:
return jsonify('')
query = evs.Action.query.filter(
evs.Action.type.in_(('Snapshot', 'Live', 'Allocate', 'Deallocate')))
return self.generate_post_csv(query)
def generate_post_csv(self, query):
d = {}
for ac in query:
create = '{}-{}'.format(ac.created.year, ac.created.month)
user = ac.author.email
if user not in d:
d[user] = {}
if create not in d[user]:
d[user][create] = []
d[user][create].append(ac)
data = StringIO()
cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
cw.writerow(InternalStatsRow('', "2000-1", []).keys())
for user, createds in d.items():
for create, actions in createds.items():
cw.writerow(InternalStatsRow(user, create, actions).values())
bfile = data.getvalue().encode('utf-8')
output = make_response(bfile)
insert_hash(bfile)
output.headers['Content-Disposition'] = 'attachment; filename=internal-stats.csv'
output.headers['Content-type'] = 'text/csv'
return output
class WbConfDocumentView(DeviceView): class WbConfDocumentView(DeviceView):
@ -386,10 +382,11 @@ class WbConfDocumentView(DeviceView):
if not wbtype.lower() in ['usodyrate', 'usodywipe']: if not wbtype.lower() in ['usodyrate', 'usodywipe']:
return jsonify('') return jsonify('')
data = {'token': self.get_token(), data = {
'host': app.config['HOST'], 'token': self.get_token(),
'inventory': app.config['SCHEMA'] 'host': app.config['HOST'],
} 'inventory': app.config['SCHEMA'],
}
data['erase'] = False data['erase'] = False
# data['erase'] = True if wbtype == 'usodywipe' else False # data['erase'] = True if wbtype == 'usodywipe' else False
@ -424,18 +421,31 @@ class DocumentDef(Resource):
VIEW = None # We do not want to create default / documents endpoint VIEW = None # We do not want to create default / documents endpoint
AUTH = False AUTH = False
def __init__(self, app, def __init__(
import_name=__name__, self,
static_folder='static', app,
static_url_path=None, import_name=__name__,
template_folder='templates', static_folder='static',
url_prefix=None, static_url_path=None,
subdomain=None, template_folder='templates',
url_defaults=None, url_prefix=None,
root_path=None, subdomain=None,
cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()): url_defaults=None,
super().__init__(app, import_name, static_folder, static_url_path, template_folder, root_path=None,
url_prefix, subdomain, url_defaults, root_path, cli_commands) cli_commands: Iterable[Tuple[Callable, str or None]] = tuple(),
):
super().__init__(
app,
import_name,
static_folder,
static_url_path,
template_folder,
url_prefix,
subdomain,
url_defaults,
root_path,
cli_commands,
)
d = {'id': None} d = {'id': None}
get = {'GET'} get = {'GET'}
@ -446,12 +456,15 @@ class DocumentDef(Resource):
view = app.auth.requires_auth(view) view = app.auth.requires_auth(view)
self.add_url_rule('/erasures/', defaults=d, view_func=view, methods=get) self.add_url_rule('/erasures/', defaults=d, view_func=view, methods=get)
self.add_url_rule('/erasures/<{}:{}>'.format(self.ID_CONVERTER.value, self.ID_NAME), self.add_url_rule(
view_func=view, methods=get) '/erasures/<{}:{}>'.format(self.ID_CONVERTER.value, self.ID_NAME),
view_func=view,
methods=get,
)
devices_view = DevicesDocumentView.as_view('devicesDocumentView', devices_view = DevicesDocumentView.as_view(
definition=self, 'devicesDocumentView', definition=self, auth=app.auth
auth=app.auth) )
devices_view = app.auth.requires_auth(devices_view) devices_view = app.auth.requires_auth(devices_view)
stock_view = StockDocumentView.as_view('stockDocumentView', definition=self) stock_view = StockDocumentView.as_view('stockDocumentView', definition=self)
@ -463,7 +476,9 @@ class DocumentDef(Resource):
lots_view = app.auth.requires_auth(lots_view) lots_view = app.auth.requires_auth(lots_view)
self.add_url_rule('/lots/', defaults=d, view_func=lots_view, methods=get) self.add_url_rule('/lots/', defaults=d, view_func=lots_view, methods=get)
stock_view = StockDocumentView.as_view('stockDocumentView', definition=self, auth=app.auth) stock_view = StockDocumentView.as_view(
'stockDocumentView', definition=self, auth=app.auth
)
stock_view = app.auth.requires_auth(stock_view) stock_view = app.auth.requires_auth(stock_view)
self.add_url_rule('/stock/', defaults=d, view_func=stock_view, methods=get) self.add_url_rule('/stock/', defaults=d, view_func=stock_view, methods=get)
@ -471,22 +486,18 @@ class DocumentDef(Resource):
self.add_url_rule('/check/', defaults={}, view_func=check_view, methods=get) self.add_url_rule('/check/', defaults={}, view_func=check_view, methods=get)
stamps_view = StampsView.as_view('StampsView', definition=self, auth=app.auth) stamps_view = StampsView.as_view('StampsView', definition=self, auth=app.auth)
self.add_url_rule('/stamps/', defaults={}, view_func=stamps_view, methods={'GET', 'POST'}) self.add_url_rule(
'/stamps/', defaults={}, view_func=stamps_view, methods={'GET', 'POST'}
)
# internalstats_view = InternalStatsView.as_view( actions_view = ActionsDocumentView.as_view(
# 'InternalStatsView', definition=self, auth=app.auth) 'ActionsDocumentView', definition=self, auth=app.auth
# internalstats_view = app.auth.requires_auth(internalstats_view) )
# self.add_url_rule('/internalstats/', defaults=d, view_func=internalstats_view,
# methods=get)
actions_view = ActionsDocumentView.as_view('ActionsDocumentView',
definition=self,
auth=app.auth)
actions_view = app.auth.requires_auth(actions_view) actions_view = app.auth.requires_auth(actions_view)
self.add_url_rule('/actions/', defaults=d, view_func=actions_view, methods=get) self.add_url_rule('/actions/', defaults=d, view_func=actions_view, methods=get)
wbconf_view = WbConfDocumentView.as_view('WbConfDocumentView', wbconf_view = WbConfDocumentView.as_view(
definition=self, 'WbConfDocumentView', definition=self, auth=app.auth
auth=app.auth) )
wbconf_view = app.auth.requires_auth(wbconf_view) wbconf_view = app.auth.requires_auth(wbconf_view)
self.add_url_rule('/wbconf/<string:wbtype>', view_func=wbconf_view, methods=get) self.add_url_rule('/wbconf/<string:wbtype>', view_func=wbconf_view, methods=get)

View file

@ -1,10 +1,11 @@
import datetime import datetime
import pytest
import json import json
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from uuid import UUID from uuid import UUID
import pytest
from flask import g from flask import g
from flask.testing import FlaskClient from flask.testing import FlaskClient
from flask_wtf.csrf import generate_csrf from flask_wtf.csrf import generate_csrf