diff --git a/CHANGELOG.md b/CHANGELOG.md index 391163d2..141fb297 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ml). [1.0.5-beta] ## [1.0.5-beta] +- [addend] #124 adding endpoint for extract the internal stats of use ## [1.0.4-beta] - [addend] #95 adding endpoint for check the hash of one report diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index 449f104d..beef8b25 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -65,3 +65,6 @@ class DevicehubConfig(Config): PRICE_VERSION = StrictVersion('1.0') PRICE_CURRENCY = Currency.EUR """Official versions.""" + + """Admin email""" + EMAIL_ADMIN = config('EMAIL_ADMIN', '') diff --git a/ereuse_devicehub/resources/documents/device_row.py b/ereuse_devicehub/resources/documents/device_row.py index 91c96000..74a322be 100644 --- a/ereuse_devicehub/resources/documents/device_row.py +++ b/ereuse_devicehub/resources/documents/device_row.py @@ -379,3 +379,84 @@ class ActionRow(OrderedDict): self['Type'] = allocate['type'] self['LiveCreate'] = allocate['liveCreate'] self['UsageTimeHdd'] = allocate['usageTimeHdd'] + + +class InternalStatsRow(OrderedDict): + + def __init__(self, user, create, actions): + super().__init__() + # General information about all internal stats + # user, quart, month, year: + # Snapshot (Registers) + # Snapshots (Update) + # Snapshots (All) + # Allocate + # Deallocate + # Live + self.actions = actions + year, month = create.split('-') + + 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['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(ac) + ) + ) + ) + + def is_allocate(self, ac): + if ac.type == 'Allocate': + self['Allocates'] += 1 + 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)] diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index 9d74ca10..cc47fd30 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -5,6 +5,7 @@ import uuid from collections import OrderedDict from io import StringIO from typing import Callable, Iterable, Tuple +from decouple import config import boltons import flask @@ -12,6 +13,7 @@ import flask_weasyprint import teal.marshmallow from boltons import urlutils from flask import make_response, g, request +from flask import current_app as app from flask.json import jsonify from teal.cache import cache from teal.resource import Resource, View @@ -21,7 +23,8 @@ from ereuse_devicehub.resources.action import models as evs from ereuse_devicehub.resources.device import models as devs from ereuse_devicehub.resources.deliverynote.models import Deliverynote from ereuse_devicehub.resources.device.views import DeviceView -from ereuse_devicehub.resources.documents.device_row import DeviceRow, StockRow, ActionRow +from ereuse_devicehub.resources.documents.device_row import (DeviceRow, StockRow, ActionRow, + InternalStatsRow) from ereuse_devicehub.resources.lot import LotView from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.hash_reports import insert_hash, ReportHash, verify_hash @@ -215,7 +218,7 @@ class StockDocumentView(DeviceView): def generate_post_csv(self, query): """Get device query and put information in csv format.""" data = StringIO() - cw = csv.writer(data) + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') first = True for device in query: d = StockRow(device) @@ -277,6 +280,43 @@ class StampsView(View): 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 not user in d: + d[user] = {} + if not create 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 DocumentDef(Resource): __type__ = 'Document' SCHEMA = None @@ -332,8 +372,14 @@ class DocumentDef(Resource): stamps_view = StampsView.as_view('StampsView', definition=self, auth=app.auth) self.add_url_rule('/stamps/', defaults={}, view_func=stamps_view, methods={'GET', 'POST'}) - actions_view = ActionsDocumentView.as_view('ActionsDocumentView', - definition=self, + internalstats_view = InternalStatsView.as_view( + 'InternalStatsView', 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) self.add_url_rule('/actions/', defaults=d, view_func=actions_view, methods=get) diff --git a/tests/conftest.py b/tests/conftest.py index d212a9b7..d0a0bdd2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,7 @@ class TestConfig(DevicehubConfig): SERVER_NAME = 'localhost' TMP_SNAPSHOTS = '/tmp/snapshots' TMP_LIVES = '/tmp/lives' + EMAIL_ADMIN = 'foo@foo.com' @pytest.fixture(scope='session') diff --git a/tests/test_basic.py b/tests/test_basic.py index 77103f1b..11ffbb80 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -40,6 +40,7 @@ def test_api_docs(client: Client): '/documents/erasures/', '/documents/devices/', '/documents/stamps/', + '/documents/internalstats/', '/documents/stock/', '/documents/check/', '/documents/lots/', diff --git a/tests/test_documents.py b/tests/test_documents.py index c23ab6ba..645bf51c 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -415,6 +415,8 @@ def test_report_devices_stock_control(user: UserClient, user2: UserClient): assert user.user['id'] != user2.user['id'] assert len(export_csv) == 2 + export_csv[0] = export_csv[0][0].split(';') + export_csv[1] = export_csv[1][0].split(';') assert isinstance(datetime.strptime(export_csv[1][5], '%c'), datetime), \ 'Register in field is not a datetime' @@ -614,3 +616,32 @@ def test_verify_stamp_erasure_certificate(user: UserClient, client: Client): 'example.csv')]}, status=200) assert "alert alert-info" in response + + +@pytest.mark.mvp +def test_get_document_internal_stats(user: UserClient, user2: UserClient): + """Tests for get teh internal stats.""" + + # csv_str, _ = user.get(res=documents.DocumentDef.t, + # item='internalstats/') + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='internalstats/', + accept='text/csv', + query=[]) + + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + + assert len(export_csv) == 1 + + csv_str, _ = user2.get(res=documents.DocumentDef.t, + item='internalstats/', + accept='text/csv', + query=[]) + + f = StringIO(csv_str) + obj_csv = csv.reader(f, f) + export_csv = list(obj_csv) + + assert csv_str.strip() == '""'