diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index f2ab43d3..bbe229d6 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -1,14 +1,10 @@ -import csv import datetime -from io import StringIO import marshmallow -from flask import current_app as app, render_template, request, make_response +from flask import current_app as app, render_template, request from flask.json import jsonify from flask_sqlalchemy import Pagination from marshmallow import fields, fields as f, validate as v -from sqlalchemy.orm import aliased -from sqlalchemy.util import OrderedDict from teal import query from teal.cache import cache from teal.resource import View @@ -17,9 +13,7 @@ from ereuse_devicehub import auth from ereuse_devicehub.db import db from ereuse_devicehub.query import SearchQueryParser, things_response from ereuse_devicehub.resources import search -from ereuse_devicehub.resources.device.models import Component, Computer, Device, Manufacturer, \ - Display, Processor, GraphicCard, Motherboard, NetworkAdapter, DataStorage, RamModule, \ - SoundCard +from ereuse_devicehub.resources.device.models import Device, Manufacturer from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.event import models as events from ereuse_devicehub.resources.lot.models import LotDeviceDescendants @@ -138,150 +132,7 @@ class DeviceView(View): ).order_by( search.Search.rank(properties, search_p) + search.Search.rank(tags, search_p) ) - query = query.filter(*args['filter']).order_by(*args['sort']) - if 'text/csv' in request.accept_mimetypes: - return self.generate_post_csv(query) - else: - devices = query.paginate(page=args['page'], per_page=30) # type: Pagination - ret = { - 'items': self.schema.dump(devices.items, many=True, nested=1), - # todo pagination should be in Header like github - # https://developer.github.com/v3/guides/traversing-with-pagination/ - 'pagination': { - 'page': devices.page, - 'perPage': devices.per_page, - 'total': devices.total, - 'previous': devices.prev_num, - 'next': devices.next_num - }, - 'url': request.path - } - return jsonify(ret) - - def generate_post_csv(self, query): - """ - Get device query and put information in csv format - :param query: - :return: - """ - data = StringIO() - cw = csv.writer(data) - first = True - for device in query: - d = DeviceRow(device) - if first: - cw.writerow(name for name in d.keys()) - first = False - cw.writerow(v for v in d.values()) - output = make_response(data.getvalue()) - output.headers['Content-Disposition'] = 'attachment; filename=export.csv' - output.headers['Content-type'] = 'text/csv' - return output - - -class DeviceRow(OrderedDict): - NUMS = { - Display.t: 1, - Processor.t: 2, - GraphicCard.t: 2, - Motherboard.t: 1, - NetworkAdapter.t: 2, - SoundCard.t: 2 - } - - def __init__(self, device: Device) -> None: - super().__init__() - self.device = device - # General information about device - self['Type'] = device.t - if isinstance(device, Computer): - self['Chassis'] = device.chassis - self['Tag 1'] = self['Tag 2'] = self['Tag 3'] = '' - for i, tag in zip(range(1, 3), device.tags): - self['Tag {}'.format(i)] = format(tag) - self['Serial Number'] = device.serial_number - self['Model'] = device.model - self['Manufacturer'] = device.manufacturer - # self['State'] = device.last_event_of() - self['Price'] = device.price - self['Registered in'] = format(device.created, '%c') - if isinstance(device, Computer): - self['Processor'] = device.processor_model - self['RAM (GB)'] = device.ram_size - self['Storage Size (MB)'] = device.data_storage_size - rate = device.rate - if rate: - self['Rate'] = rate.rating - self['Range'] = rate.rating_range - self['Processor Rate'] = rate.processor - self['Processor Range'] = rate.workbench.processor_range - self['RAM Rate'] = rate.ram - self['RAM Range'] = rate.workbench.ram_range - self['Data Storage Rate'] = rate.data_storage - self['Data Storage Range'] = rate.workbench.data_storage_range - # More specific information about components - if isinstance(device, Computer): - self.components() - - - def components(self): - """ - Function to get all components information of a device - """ - assert isinstance(self.device, Computer) - # todo put an input specific order (non alphabetic) - for type in sorted(app.resources[Component.t].subresources_types): # type: str - max = self.NUMS.get(type, 4) - if type not in ['Component', 'HardDrive', 'SolidStateDrive']: - i = 1 - for component in (r for r in self.device.components if r.type == type): - self.fill_component(type, i, component) - i += 1 - if i > max: - break - while i <= max: - self.fill_component(type, i) - i += 1 - - def fill_component(self, type, i, component=None): - """ - Function to put specific information of components in OrderedDict (csv) - :param type: type of component - :param component: device.components - """ - self['{} {}'.format(type, i)] = format(component) if component else '' - self['{} {} Manufacturer'.format(type, i)] = component.serial_number if component else '' - self['{} {} Model'.format(type, i)] = component.serial_number if component else '' - self['{} {} Serial Number'.format(type, i)] = component.serial_number if component else '' - - """ Particular fields for component GraphicCard """ - if isinstance(component, GraphicCard): - self['{} {} Memory (MB)'.format(type, i)] = component.memory - - """ Particular fields for component DataStorage.t -> (HardDrive, SolidStateDrive) """ - if isinstance(component, DataStorage): - self['{} {} Size (MB)'.format(type, i)] = component.size - self['{} {} Privacy'.format(type, i)] = component.privacy - - # todo decide if is relevant more info about Motherboard - """ Particular fields for component Motherboard """ - if isinstance(component, Motherboard): - self['{} {} Slots'.format(type, i)] = component.slots - - """ Particular fields for component Processor """ - if isinstance(component, Processor): - self['{} {} Number of cores'.format(type, i)] = component.cores - self['{} {} Speed (GHz)'.format(type, i)] = component.speed - - """ Particular fields for component RamModule """ - if isinstance(component, RamModule): - self['{} {} Size (MB)'.format(type, i)] = component.size - self['{} {} Speed (MHz)'.format(type, i)] = component.speed - self['{} {} Size'.format(type, i)] = component.size - - # todo add Display size, ... - # todo add NetworkAdapter speedLink? - # todo add some ComputerAccessories + return query.filter(*args['filter']).order_by(*args['sort']) class ManufacturerView(View): diff --git a/ereuse_devicehub/resources/documents/device_row.py b/ereuse_devicehub/resources/documents/device_row.py new file mode 100644 index 00000000..5012da29 --- /dev/null +++ b/ereuse_devicehub/resources/documents/device_row.py @@ -0,0 +1,109 @@ +from collections import OrderedDict + +from flask import current_app + +from ereuse_devicehub.resources.device import models as d + + +class DeviceRow(OrderedDict): + NUMS = { + d.Display.t: 1, + d.Processor.t: 2, + d.GraphicCard.t: 2, + d.Motherboard.t: 1, + d.NetworkAdapter.t: 2, + d.SoundCard.t: 2 + } + + def __init__(self, device: d.Device) -> None: + super().__init__() + self.device = device + # General information about device + self['Type'] = device.t + if isinstance(device, d.Computer): + self['Chassis'] = device.chassis + self['Tag 1'] = self['Tag 2'] = self['Tag 3'] = '' + for i, tag in zip(range(1, 3), device.tags): + self['Tag {}'.format(i)] = format(tag) + self['Serial Number'] = device.serial_number + self['Model'] = device.model + self['Manufacturer'] = device.manufacturer + # self['State'] = device.last_event_of() + self['Price'] = device.price + self['Registered in'] = format(device.created, '%c') + if isinstance(device, d.Computer): + self['Processor'] = device.processor_model + self['RAM (GB)'] = device.ram_size + self['Storage Size (MB)'] = device.data_storage_size + rate = device.rate + if rate: + self['Rate'] = rate.rating + self['Range'] = rate.rating_range + self['Processor Rate'] = rate.processor + self['Processor Range'] = rate.workbench.processor_range + self['RAM Rate'] = rate.ram + self['RAM Range'] = rate.workbench.ram_range + self['Data Storage Rate'] = rate.data_storage + self['Data Storage Range'] = rate.workbench.data_storage_range + # More specific information about components + if isinstance(device, d.Computer): + self.components() + + def components(self): + """ + Function to get all components information of a device + """ + assert isinstance(self.device, d.Computer) + # todo put an input specific order (non alphabetic) + for type in sorted(current_app.resources[d.Component.t].subresources_types): # type: str + max = self.NUMS.get(type, 4) + if type not in ['Component', 'HardDrive', 'SolidStateDrive']: + i = 1 + for component in (r for r in self.device.components if r.type == type): + self.fill_component(type, i, component) + i += 1 + if i > max: + break + while i <= max: + self.fill_component(type, i) + i += 1 + + def fill_component(self, type, i, component=None): + """ + Function to put specific information of components in OrderedDict (csv) + :param type: type of component + :param component: device.components + """ + self['{} {}'.format(type, i)] = format(component) if component else '' + self['{} {} Manufacturer'.format(type, i)] = component.serial_number if component else '' + self['{} {} Model'.format(type, i)] = component.serial_number if component else '' + self['{} {} Serial Number'.format(type, i)] = component.serial_number if component else '' + + """ Particular fields for component GraphicCard """ + if isinstance(component, d.GraphicCard): + self['{} {} Memory (MB)'.format(type, i)] = component.memory + + """ Particular fields for component DataStorage.t -> (HardDrive, SolidStateDrive) """ + if isinstance(component, d.DataStorage): + self['{} {} Size (MB)'.format(type, i)] = component.size + self['{} {} Privacy'.format(type, i)] = component.privacy + + # todo decide if is relevant more info about Motherboard + """ Particular fields for component Motherboard """ + if isinstance(component, d.Motherboard): + self['{} {} Slots'.format(type, i)] = component.slots + + """ Particular fields for component Processor """ + if isinstance(component, d.Processor): + self['{} {} Number of cores'.format(type, i)] = component.cores + self['{} {} Speed (GHz)'.format(type, i)] = component.speed + + """ Particular fields for component RamModule """ + if isinstance(component, d.RamModule): + self['{} {} Size (MB)'.format(type, i)] = component.size + self['{} {} Speed (MHz)'.format(type, i)] = component.speed + self['{} {} Size'.format(type, i)] = component.size + + # todo add Display size, ... + # todo add NetworkAdapter speedLink? + # todo add some ComputerAccessories diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index 34121d8c..72582a21 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -1,5 +1,8 @@ +import csv +import datetime import enum import uuid +from io import StringIO from typing import Callable, Iterable, Tuple import boltons @@ -7,11 +10,14 @@ import flask import flask_weasyprint import teal.marshmallow from boltons import urlutils +from flask import make_response +from teal.cache import cache from teal.resource import Resource from ereuse_devicehub.db import db from ereuse_devicehub.resources.device import models as devs from ereuse_devicehub.resources.device.views import DeviceView +from ereuse_devicehub.resources.documents.device_row import DeviceRow from ereuse_devicehub.resources.event import models as evs @@ -97,6 +103,33 @@ class DocumentView(DeviceView): return flask.render_template('documents/erasure.html', **params) +class DevicesDocumentView(DeviceView): + @cache(datetime.timedelta(minutes=1)) + def find(self, args: dict): + query = self.query(args) + return self.generate_post_csv(query) + + def generate_post_csv(self, query): + """ + Get device query and put information in csv format + :param query: + :return: + """ + data = StringIO() + cw = csv.writer(data) + first = True + for device in query: + d = DeviceRow(device) + if first: + cw.writerow(name for name in d.keys()) + first = False + cw.writerow(v for v in d.values()) + output = make_response(data.getvalue()) + output.headers['Content-Disposition'] = 'attachment; filename=export.csv' + output.headers['Content-type'] = 'text/csv' + return output + + class DocumentDef(Resource): __type__ = 'Document' SCHEMA = None @@ -124,3 +157,9 @@ class DocumentDef(Resource): 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), view_func=view, methods=get) + devices_view = DevicesDocumentView.as_view('devicesDocumentView', + definition=self, + auth=app.auth) + if self.AUTH: + devices_view = app.auth.requires_auth(devices_view) + self.add_url_rule('/devices/', defaults=d, view_func=devices_view, methods=get) diff --git a/tests/test_basic.py b/tests/test_basic.py index 711dbf92..f211503e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -28,6 +28,7 @@ def test_api_docs(client: Client): '/lots/{id}/children', '/lots/{id}/devices', '/documents/erasures/', + '/documents/devices/', '/documents/static/{filename}', '/tags/{tag_id}/device/{device_id}', '/devices/static/{filename}' diff --git a/tests/test_reports.py b/tests/test_reports.py index feb216d0..aa6baf59 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -3,8 +3,10 @@ from datetime import datetime from io import StringIO from pathlib import Path +import pytest + from ereuse_devicehub.client import UserClient -from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.documents import documents as docs from ereuse_devicehub.resources.event.models import Snapshot from tests.conftest import file @@ -14,7 +16,8 @@ def test_export_basic_snapshot(user: UserClient): Test export device information in a csv file """ snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot) - csv_str, _ = user.get(res=Device, + csv_str, _ = user.get(res=docs.DocumentDef.t, + item='devices/', accept='text/csv', query=[('filter', {'type': ['Computer']})]) f = StringIO(csv_str) @@ -40,7 +43,8 @@ def test_export_full_snapshot(user: UserClient): Test a export device with all information and a lot of components """ snapshot, _ = user.post(file('real-eee-1001pxd.snapshot.11'), res=Snapshot) - csv_str, _ = user.get(res=Device, + csv_str, _ = user.get(res=docs.DocumentDef.t, + item='devices/', accept='text/csv', query=[('filter', {'type': ['Computer']})]) f = StringIO(csv_str) @@ -67,7 +71,9 @@ def test_export_empty(user: UserClient): """ Test to check works correctly exporting csv without any information (no snapshot) """ - csv_str, _ = user.get(res=Device, accept='text/csv') + csv_str, _ = user.get(res=docs.DocumentDef.t, + accept='text/csv', + item='devices/') f = StringIO(csv_str) obj_csv = csv.reader(f, f) export_csv = list(obj_csv) @@ -80,7 +86,8 @@ def test_export_computer_monitor(user: UserClient): Test a export device type computer monitor """ snapshot, _ = user.post(file('computer-monitor.snapshot'), res=Snapshot) - csv_str, _ = user.get(res=Device, + csv_str, _ = user.get(res=docs.DocumentDef.t, + item='devices/', accept='text/csv', query=[('filter', {'type': ['ComputerMonitor']})]) f = StringIO(csv_str) @@ -105,7 +112,8 @@ def test_export_keyboard(user: UserClient): Test a export device type keyboard """ snapshot, _ = user.post(file('keyboard.snapshot'), res=Snapshot) - csv_str, _ = user.get(res=Device, + csv_str, _ = user.get(res=docs.DocumentDef.t, + item='devices/', accept='text/csv', query=[('filter', {'type': ['Keyboard']})]) f = StringIO(csv_str) @@ -124,6 +132,7 @@ def test_export_keyboard(user: UserClient): assert fixture_csv[1] == export_csv[1], 'Component information are not equal' +@pytest.mark.xfail(reason='Develop test') def test_export_multiple_devices(user: UserClient): """ Test a export multiple devices with different components and information @@ -131,6 +140,7 @@ def test_export_multiple_devices(user: UserClient): pass +@pytest.mark.xfail(reason='Develop test') def test_export_only_components(user: UserClient): """ Test a export only components @@ -138,7 +148,8 @@ def test_export_only_components(user: UserClient): pass -def test_export_computers__and_components(user: UserClient): +@pytest.mark.xfail(reason='Develop test') +def test_export_computers_and_components(user: UserClient): """ Test a export multiple devices (computers and independent components) """