From 03871b4462013a92949e278a1250d8f56d35edbc Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Thu, 18 Oct 2018 10:09:10 +0200 Subject: [PATCH 1/7] Fix tags not added to device search --- ereuse_devicehub/resources/device/models.py | 4 +++ ereuse_devicehub/resources/device/search.py | 29 ++++++++++++--------- ereuse_devicehub/resources/device/sync.py | 2 +- tests/test_device.py | 22 ---------------- tests/test_device_find.py | 16 ++++++++++++ tests/test_event.py | 15 ++++++++--- tests/test_rate.py | 2 +- tests/test_rate_workbench_v1.py | 10 +++---- tests/test_tag.py | 14 ++++++++-- 9 files changed, 68 insertions(+), 46 deletions(-) diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index cc57ed74..d59eab1c 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -226,6 +226,10 @@ class Computer(Device): id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) chassis = Column(DBEnum(ComputerChassis), nullable=False) + def __init__(self, chassis, **kwargs) -> None: + chassis = ComputerChassis(chassis) + super().__init__(chassis=chassis, **kwargs) + @property def events(self) -> list: return sorted(chain(super().events, self.events_parent), key=attrgetter('created')) diff --git a/ereuse_devicehub/resources/device/search.py b/ereuse_devicehub/resources/device/search.py index 8d330bfc..defd7cb7 100644 --- a/ereuse_devicehub/resources/device/search.py +++ b/ereuse_devicehub/resources/device/search.py @@ -1,3 +1,5 @@ +from itertools import chain + import inflection from sqlalchemy.dialects import postgresql from sqlalchemy.dialects.postgresql import TSVECTOR @@ -37,21 +39,24 @@ class DeviceSearch(db.Model): @classmethod def update_modified_devices(cls, session: db.Session): - """Updates the documents of the devices that are part of a modified - event in the passed-in session. + """Updates the documents of the devices that are part of a + modified event, or tag in the passed-in session. - This method is registered as a SQLAlchemy - listener in the Devicehub class. + This method is registered as a SQLAlchemy listener in the + Devicehub class. """ devices_to_update = set() - for event in (e for e in session.new if isinstance(e, Event)): - if isinstance(event, EventWithMultipleDevices): - devices_to_update |= event.devices - elif isinstance(event, EventWithOneDevice): - devices_to_update.add(event.device) - if event.parent: - devices_to_update.add(event.parent) - devices_to_update |= event.components + for model in chain(session.new, session.dirty): + if isinstance(model, Event): + if isinstance(model, EventWithMultipleDevices): + devices_to_update |= model.devices + elif isinstance(model, EventWithOneDevice): + devices_to_update.add(model.device) + if model.parent: + devices_to_update.add(model.parent) + devices_to_update |= model.components + elif isinstance(model, Tag) and model.device: + devices_to_update.add(model.device) # this flush is controversial: # see https://groups.google.com/forum/#!topic/sqlalchemy/hBzfypgPfYo diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index 7f883ab1..156b8734 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -229,7 +229,7 @@ class Sync: if adding: # For the components we are adding, let's remove them from their old parents def g_parent(component: Component) -> Device: - return component.parent or Computer(id=0) # Computer with id 0 is our Identity + return component.parent or Device(id=0) # Computer with id 0 is our Identity for parent, _components in groupby(sorted(adding, key=g_parent), key=g_parent): if parent.id != 0: # Is not Computer Identity diff --git a/tests/test_device.py b/tests/test_device.py index a630f490..2ea2d9e1 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -18,7 +18,6 @@ from ereuse_devicehub.resources.device.exceptions import NeedsId from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, DataStorage, \ Desktop, Device, GraphicCard, Laptop, Motherboard, NetworkAdapter from ereuse_devicehub.resources.device.schemas import Device as DeviceS -from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \ Sync from ereuse_devicehub.resources.enums import ComputerChassis, DisplayTech @@ -474,20 +473,6 @@ def test_computer_with_display(): pass -def test_device_search_all_devices_token_if_empty(app: Devicehub, user: UserClient): - """Ensures DeviceSearch can regenerate itself when the table is empty.""" - user.post(file('basic.snapshot'), res=m.Snapshot) - with app.app_context(): - app.db.session.execute('TRUNCATE TABLE {}'.format(DeviceSearch.__table__.name)) - app.db.session.commit() - i, _ = user.get(res=Device, query=[('search', 'Desktop')]) - assert not len(i['items']) - with app.app_context(): - DeviceSearch.set_all_devices_tokens_if_empty(app.db.session) - i, _ = user.get(res=Device, query=[('search', 'Desktop')]) - assert not len(i['items']) - - def test_manufacturer(user: UserClient): m, r = user.get(res='Manufacturer', query=[('name', 'asus')]) assert m == {'items': [{'name': 'Asus', 'url': 'https://en.wikipedia.org/wiki/Asus'}]} @@ -528,10 +513,3 @@ def test_device_public(user: UserClient, client: Client): html, _ = client.get(res=Device, item=s['device']['id'], accept=ANY) assert 'intel atom cpu n270 @ 1.60ghz' in html assert 'S/N 00:24:8C:7F:CF:2D – 100 Mbps' in html - - -@pytest.mark.xfail(reason='Functionality not yet developed.') -def test_device_search_multiple_tags(user: UserClient): - """Ensures that users can search multiple tags at once - and get their multiple devices.""" - pass diff --git a/tests/test_device_find.py b/tests/test_device_find.py index 484957ae..e5fd3807 100644 --- a/tests/test_device_find.py +++ b/tests/test_device_find.py @@ -6,6 +6,7 @@ from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.device.models import Desktop, Device, Laptop, Processor, \ SolidStateDrive +from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.device.views import Filters, Sorting from ereuse_devicehub.resources.enums import ComputerChassis from ereuse_devicehub.resources.event.models import Snapshot @@ -174,6 +175,21 @@ def test_device_lots_query(user: UserClient): pass +def test_device_search_all_devices_token_if_empty(app: Devicehub, user: UserClient): + """Ensures DeviceSearch can regenerate itself when the table is empty.""" + user.post(file('basic.snapshot'), res=Snapshot) + with app.app_context(): + app.db.session.execute('TRUNCATE TABLE {}'.format(DeviceSearch.__table__.name)) + app.db.session.commit() + i, _ = user.get(res=Device, query=[('search', 'Desktop')]) + assert not len(i['items']) + with app.app_context(): + DeviceSearch.set_all_devices_tokens_if_empty(app.db.session) + app.db.session.commit() + i, _ = user.get(res=Device, query=[('search', 'Desktop')]) + assert i['items'] + + def test_device_query_search(user: UserClient): # todo improve user.post(file('basic.snapshot'), res=Snapshot) diff --git a/tests/test_event.py b/tests/test_event.py index 79f52d53..687bb34f 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -112,7 +112,10 @@ def test_install(): @pytest.mark.usefixtures(conftest.auth_app_context.__name__) def test_update_components_event_one(): - computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1') + computer = Desktop(serial_number='sn1', + model='ml1', + manufacturer='mr1', + chassis=ComputerChassis.Tower) hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar') computer.components.add(hdd) @@ -137,7 +140,10 @@ def test_update_components_event_one(): @pytest.mark.usefixtures(conftest.auth_app_context.__name__) def test_update_components_event_multiple(): - computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1') + computer = Desktop(serial_number='sn1', + model='ml1', + manufacturer='mr1', + chassis=ComputerChassis.Tower) hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar') computer.components.add(hdd) @@ -163,7 +169,10 @@ def test_update_components_event_multiple(): @pytest.mark.usefixtures(conftest.auth_app_context.__name__) def test_update_parent(): - computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1') + computer = Desktop(serial_number='sn1', + model='ml1', + manufacturer='mr1', + chassis=ComputerChassis.Tower) hdd = HardDrive(serial_number='foo', manufacturer='bar', model='foo-bar') computer.components.add(hdd) diff --git a/tests/test_rate.py b/tests/test_rate.py index 99a9420b..7f66cff4 100644 --- a/tests/test_rate.py +++ b/tests/test_rate.py @@ -51,7 +51,7 @@ def test_rate(): appearance_range=AppearanceRange.A, functionality_range=FunctionalityRange.A ) - pc = Desktop() + pc = Desktop(chassis=ComputerChassis.Tower) hdd = HardDrive(size=476940) hdd.events_one.add(BenchmarkDataStorage(read_speed=126, write_speed=29.8)) cpu = Processor(cores=2, speed=3.4) diff --git a/tests/test_rate_workbench_v1.py b/tests/test_rate_workbench_v1.py index a078e797..da4a0a09 100644 --- a/tests/test_rate_workbench_v1.py +++ b/tests/test_rate_workbench_v1.py @@ -15,7 +15,7 @@ Excluded cases in tests import pytest from ereuse_devicehub.resources.device.models import Desktop, HardDrive, Processor, RamModule -from ereuse_devicehub.resources.enums import AppearanceRange, FunctionalityRange +from ereuse_devicehub.resources.enums import AppearanceRange, ComputerChassis, FunctionalityRange from ereuse_devicehub.resources.event.models import BenchmarkDataStorage, BenchmarkProcessor, \ WorkbenchRate from ereuse_devicehub.resources.event.rate.workbench.v1_0 import DataStorageRate, ProcessorRate, \ @@ -307,7 +307,7 @@ def test_rate_computer_rate(): """ # Create a new Computer with components characteristics of pc with id = 1193 - pc_test = Desktop() + pc_test = Desktop(chassis=ComputerChassis.Tower) data_storage = HardDrive(size=476940) data_storage.events_one.add(BenchmarkDataStorage(read_speed=126, write_speed=29.8)) cpu = Processor(cores=2, speed=3.4) @@ -333,7 +333,7 @@ def test_rate_computer_rate(): assert round(rate_pc.rating, 2) == 4.61 # Create a new Computer with components characteristics of pc with id = 1201 - pc_test = Desktop() + pc_test = Desktop(chassis=ComputerChassis.Tower) data_storage = HardDrive(size=476940) data_storage.events_one.add(BenchmarkDataStorage(read_speed=158, write_speed=34.7)) cpu = Processor(cores=2, speed=3.3) @@ -358,7 +358,7 @@ def test_rate_computer_rate(): assert round(rate_pc.rating, 2) == 3.48 # Create a new Computer with components characteristics of pc with id = 79 - pc_test = Desktop() + pc_test = Desktop(chassis=ComputerChassis.Tower) data_storage = HardDrive(size=76319) data_storage.events_one.add(BenchmarkDataStorage(read_speed=72.2, write_speed=24.3)) cpu = Processor(cores=1, speed=1.6) @@ -386,7 +386,7 @@ def test_rate_computer_rate(): assert round(rate_pc.rating, 2) == 1.58 # Create a new Computer with components characteristics of pc with id = 798 - pc_test = Desktop() + pc_test = Desktop(chassis=ComputerChassis.Tower) data_storage = HardDrive(size=152587) data_storage.events_one.add(BenchmarkDataStorage(read_speed=78.1, write_speed=24.4)) cpu = Processor(cores=2, speed=2.5) diff --git a/tests/test_tag.py b/tests/test_tag.py index e20c2a7c..476cdd18 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -155,8 +155,11 @@ def test_tag_create_etags_cli(app: Devicehub, user: UserClient): assert tag.provider == URL('https://t.ereuse.org') -def test_tag_manual_link(app: Devicehub, user: UserClient): - """Tests linking manually a tag through PUT /tags//device/""" +def test_tag_manual_link_search(app: Devicehub, user: UserClient): + """Tests linking manually a tag through PUT /tags//device/ + + Checks search has the term. + """ with app.app_context(): db.session.add(Tag('foo-bar', secondary='foo-sec')) desktop = Desktop(serial_number='foo', chassis=ComputerChassis.AllInOne) @@ -179,6 +182,13 @@ def test_tag_manual_link(app: Devicehub, user: UserClient): # cannot link to another device when already linked user.put({}, res=Tag, item='foo-bar/device/99', status=LinkedToAnotherDevice) + i, _ = user.get(res=Device, query=[('search', 'foo-bar')]) + assert i['items'] + i, _ = user.get(res=Device, query=[('search', 'foo-sec')]) + assert i['items'] + i, _ = user.get(res=Device, query=[('search', 'foo')]) + assert i['items'] + @pytest.mark.usefixtures(conftest.app_context.__name__) def test_tag_secondary_workbench_link_find(user: UserClient): From 6751f0db16bc0bc938c9251a9745300b24dfa743 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Thu, 18 Oct 2018 13:08:42 +0200 Subject: [PATCH 2/7] Change manufacturer endpoint query name for search --- ereuse_devicehub/resources/device/views.py | 10 +++++----- tests/test_device.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index 2c143517..783d041e 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -139,15 +139,15 @@ class DeviceView(View): class ManufacturerView(View): class FindArgs(marshmallow.Schema): - name = marshmallow.fields.Str(required=True, - # Disallow like operators - validate=lambda x: '%' not in x and '_' not in x) + search = marshmallow.fields.Str(required=True, + # Disallow like operators + validate=lambda x: '%' not in x and '_' not in x) @cache(datetime.timedelta(days=1)) def find(self, args: dict): - name = args['name'] + search = args['search'] manufacturers = Manufacturer.query \ - .filter(Manufacturer.name.ilike(name + '%')) \ + .filter(Manufacturer.name.ilike(search + '%')) \ .paginate(page=1, per_page=6) # type: Pagination return jsonify( items=app.resources[Manufacturer.t].schema.dump( diff --git a/tests/test_device.py b/tests/test_device.py index 2ea2d9e1..2c69620b 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -474,7 +474,7 @@ def test_computer_with_display(): def test_manufacturer(user: UserClient): - m, r = user.get(res='Manufacturer', query=[('name', 'asus')]) + m, r = user.get(res='Manufacturer', query=[('search', 'asus')]) assert m == {'items': [{'name': 'Asus', 'url': 'https://en.wikipedia.org/wiki/Asus'}]} assert r.cache_control.public assert r.expires > datetime.datetime.now() From 7ad7ab1d8a12ec1ced0da5d7b80ac12b0f56404a Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Fri, 19 Oct 2018 10:35:23 +0200 Subject: [PATCH 3/7] PATCH lot name --- ereuse_devicehub/resources/lot/views.py | 10 +++++++++- tests/test_lot.py | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ereuse_devicehub/resources/lot/views.py b/ereuse_devicehub/resources/lot/views.py index 9b2707dc..5532a2e5 100644 --- a/ereuse_devicehub/resources/lot/views.py +++ b/ereuse_devicehub/resources/lot/views.py @@ -4,7 +4,7 @@ from enum import Enum from typing import List, Set import marshmallow as ma -from flask import jsonify, request +from flask import Response, jsonify, request from marshmallow import Schema as MarshmallowSchema, fields as f from teal.marshmallow import EnumField from teal.resource import View @@ -36,6 +36,14 @@ class LotView(View): ret.status_code = 201 return ret + def patch(self, id): + l = request.get_json() + lot = Lot.query.filter_by(id=id).one() + for key, value in l.items(): + setattr(lot, key, value) + db.session.commit() + return Response(status=204) + def one(self, id: uuid.UUID): """Gets one event.""" lot = Lot.query.filter_by(id=id).one() # type: Lot diff --git a/tests/test_lot.py b/tests/test_lot.py index 8749ed96..f84345c2 100644 --- a/tests/test_lot.py +++ b/tests/test_lot.py @@ -23,6 +23,15 @@ In case of error, debug with: """ +def test_lot_modify_patch_endpoint(user: UserClient): + """Creates and modifies lot properties through the endpoint""" + l, _ = user.post({'name': 'foo'}, res=Lot) + assert l['name'] == 'foo' + user.patch({'name': 'bar'}, res=Lot, item=l['id'], status=204) + l_after, _ = user.get(res=Lot, item=l['id']) + assert l_after['name'] == 'bar' + + @pytest.mark.xfail(reason='Components are not added to lots!') @pytest.mark.usefixtures(conftest.auth_app_context.__name__) def test_lot_device_relationship(): From 46f765a683405a8559721c0aa6696c0f024d7c0c Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Fri, 19 Oct 2018 22:13:16 +0200 Subject: [PATCH 4/7] Bump teal to 0.2.0a26 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ac6fa543..69ac4f1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ requests==2.19.1 requests-mock==1.5.2 SQLAlchemy==1.2.11 SQLAlchemy-Utils==0.33.3 -teal==0.2.0a25 +teal==0.2.0a26 webargs==4.0.0 Werkzeug==0.14.1 sqlalchemy-citext==1.3.post0 diff --git a/setup.py b/setup.py index d986ac89..fd4aa80b 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setup( long_description=long_description, long_description_content_type='text/markdown', install_requires=[ - 'teal>=0.2.0a25', # teal always first + 'teal>=0.2.0a26', # teal always first 'click', 'click-spinner', 'ereuse-utils[Naming]>=0.4b9', From 10c73a4e75ec80ddfb7e3ce6e23b8dc22e2366c3 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Tue, 23 Oct 2018 15:37:37 +0200 Subject: [PATCH 5/7] Add ComputerAccessory, Networking, Printer, Sound, Video devices --- .../dummy/files/keyboard.snapshot.yaml | 14 + .../resources/device/definitions.py | 115 ++++++++ ereuse_devicehub/resources/device/models.py | 112 +++++++- ereuse_devicehub/resources/device/models.pyi | 108 +++++++- ereuse_devicehub/resources/device/schemas.py | 82 +++++- ereuse_devicehub/resources/enums.py | 9 + ereuse_devicehub/resources/models.pyi | 8 +- ereuse_devicehub/resources/schemas.py | 4 +- tests/test_basic.py | 2 +- tests/test_device.py | 248 ++++++++++-------- tests/test_snapshot.py | 7 + 11 files changed, 582 insertions(+), 127 deletions(-) create mode 100644 ereuse_devicehub/dummy/files/keyboard.snapshot.yaml diff --git a/ereuse_devicehub/dummy/files/keyboard.snapshot.yaml b/ereuse_devicehub/dummy/files/keyboard.snapshot.yaml new file mode 100644 index 00000000..923c2f19 --- /dev/null +++ b/ereuse_devicehub/dummy/files/keyboard.snapshot.yaml @@ -0,0 +1,14 @@ +type: Snapshot +version: '1.0' +software: Web +device: + type: Keyboard + model: FOO + serialNumber: BAR + manufacturer: BAZ + layout: ES + events: + - type: ManualRate + appearanceRange: A + functionalityRange: A + labelling: False diff --git a/ereuse_devicehub/resources/device/definitions.py b/ereuse_devicehub/resources/device/definitions.py index b0224bc3..61211ca8 100644 --- a/ereuse_devicehub/resources/device/definitions.py +++ b/ereuse_devicehub/resources/device/definitions.py @@ -161,6 +161,121 @@ class DisplayDef(ComponentDef): SCHEMA = schemas.Display +class ComputerAccessoryDef(DeviceDef): + VIEW = None + SCHEMA = schemas.ComputerAccessory + + def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None, + template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, + root_path=None, 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) + + +class MouseDef(ComputerAccessoryDef): + VIEW = None + SCHEMA = schemas.Mouse + + +class KeyboardDef(ComputerAccessoryDef): + VIEW = None + SCHEMA = schemas.Keyboard + + +class SAIDef(ComputerAccessoryDef): + VIEW = None + SCHEMA = schemas.SAI + + +class MemoryCardReaderDef(ComputerAccessoryDef): + VIEW = None + SCHEMA = schemas.MemoryCardReader + + +class NetworkingDef(DeviceDef): + VIEW = None + SCHEMA = schemas.Networking + + def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None, + template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, + root_path=None, 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) + + +class RouterDef(NetworkingDef): + VIEW = None + SCHEMA = schemas.Router + + +class SwitchDef(NetworkingDef): + VIEW = None + SCHEMA = schemas.Switch + + +class HubDef(NetworkingDef): + VIEW = None + SCHEMA = schemas.Hub + + +class WirelessAccessPointDef(NetworkingDef): + VIEW = None + SCHEMA = schemas.WirelessAccessPoint + + +class PrinterDef(DeviceDef): + VIEW = None + SCHEMA = schemas.Printer + + def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None, + template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, + root_path=None, 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) + + +class LabelPrinterDef(PrinterDef): + VIEW = None + SCHEMA = schemas.LabelPrinter + + +class SoundDef(DeviceDef): + VIEW = None + SCHEMA = schemas.Sound + + def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None, + template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, + root_path=None, 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) + + +class MicrophoneDef(SoundDef): + VIEW = None + SCHEMA = schemas.Microphone + + +class VideoDef(DeviceDef): + VIEW = None + SCHEMA = schemas.Video + + def __init__(self, app, import_name=__name__, static_folder=None, static_url_path=None, + template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, + root_path=None, 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) + + +class VideoScalerDef(VideoDef): + VIEW = None + SCHEMA = schemas.VideoScaler + + +class VideoconferenceDef(VideoDef): + VIEW = None + SCHEMA = schemas.Videoconference + + class ManufacturerDef(Resource): VIEW = ManufacturerView SCHEMA = schemas.Manufacturer diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index d59eab1c..5f72f86a 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -17,12 +17,13 @@ from sqlalchemy_utils import ColorType from stdnum import imei, meid from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, URL, check_lower, \ check_range +from teal.enums import Layouts from teal.marshmallow import ValidationError from teal.resource import url_for_resource from ereuse_devicehub.db import db from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \ - DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface + DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing @@ -62,6 +63,19 @@ class Device(Thing): """ color = Column(ColorType) color.comment = """The predominant color of the device.""" + production_date = Column(db.TIMESTAMP(timezone=True)) + production_date.comment = """The date of production of the item.""" + + _NON_PHYSICAL_PROPS = { + 'id', + 'type', + 'created', + 'updated', + 'parent_id', + 'hid', + 'production_date', + 'color' + } @property def events(self) -> list: @@ -94,7 +108,7 @@ class Device(Thing): for c in inspect(self.__class__).attrs if isinstance(c, ColumnProperty) and not getattr(c, 'foreign_keys', None) - and c.key not in {'id', 'type', 'created', 'updated', 'parent_id', 'hid'}} + and c.key not in self._NON_PHYSICAL_PROPS} @property def url(self) -> urlutils.URL: @@ -194,6 +208,11 @@ class Device(Thing): class DisplayMixin: + """ + Aspect ratio can be computed as in + https://github.com/mirukan/whratio/blob/master/whratio/ratio.py and + could be a future property. + """ size = Column(Float(decimal_return_scale=2), check_range('size', 2, 150)) size.comment = """ The size of the monitor in inches. @@ -212,6 +231,10 @@ class DisplayMixin: The maximum vertical resolution the monitor can natively support in pixels. """ + refresh_rate = Column(SmallInteger, check_range('refresh_rate', 10, 1000)) + contrast_ratio = Column(SmallInteger, check_range('contrast_ratio', 100, 100000)) + touchable = Column(Boolean, nullable=False, default=False) + touchable.comment = """Whether it is a touchscreen.""" def __format__(self, format_spec: str) -> str: v = '' @@ -289,7 +312,9 @@ class Desktop(Computer): class Laptop(Computer): - pass + layout = Column(DBEnum(Layouts)) + layout.comment = """Layout of a built-in keyboard of the computer, + if any.""" class Server(Computer): @@ -308,6 +333,10 @@ class TelevisionSet(Monitor): pass +class Projector(Monitor): + pass + + class Mobile(Device): id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) imei = Column(BigInteger) @@ -483,6 +512,83 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component): pass +class ComputerAccessory(Device): + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + pass + + +class SAI(ComputerAccessory): + pass + + +class Keyboard(ComputerAccessory): + layout = Column(DBEnum(Layouts)) # If we want to do it not null + + +class Mouse(ComputerAccessory): + pass + + +class MemoryCardReader(ComputerAccessory): + pass + + +class Networking(NetworkMixin, Device): + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + + +class Router(Networking): + pass + + +class Switch(Networking): + pass + + +class Hub(Networking): + pass + + +class WirelessAccessPoint(Networking): + pass + + +class Printer(Device): + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + wireless = Column(Boolean, nullable=False, default=False) + wireless.comment = """Whether it is a wireless printer.""" + scanning = Column(Boolean, nullable=False, default=False) + scanning.comment = """Whether the printer has scanning capabilities.""" + technology = Column(DBEnum(PrinterTechnology)) + technology.comment = """Technology used to print.""" + monochrome = Column(Boolean, nullable=False, default=True) + monochrome.comment = """Whether the printer is only monochrome.""" + + +class LabelPrinter(Printer): + pass + + +class Sound(Device): + pass + + +class Microphone(Sound): + pass + + +class Video(Device): + pass + + +class VideoScaler(Video): + pass + + +class Videoconference(Video): + pass + + class Manufacturer(db.Model): __table_args__ = {'schema': 'common'} CSV_DELIMITER = csv.get_dialect('excel').delimiter diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi index d53de794..69445b47 100644 --- a/ereuse_devicehub/resources/device/models.pyi +++ b/ereuse_devicehub/resources/device/models.pyi @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Dict, List, Set, Type, Union from boltons import urlutils @@ -6,11 +7,12 @@ from colour import Color from sqlalchemy import Column, Integer from sqlalchemy.orm import relationship from teal.db import Model +from teal.enums import Layouts from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \ - DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface + DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface from ereuse_devicehub.resources.event import models as e from ereuse_devicehub.resources.image.models import ImageList from ereuse_devicehub.resources.lot.models import Lot @@ -31,6 +33,7 @@ class Device(Thing): depth = ... # type: Column color = ... # type: Column lots = ... # type: relationship + production_date = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) @@ -52,6 +55,7 @@ class Device(Thing): self.images = ... # type: ImageList self.tags = ... # type: Set[Tag] self.lots = ... # type: Set[Lot] + self.production_date = ... # type: datetime @property def url(self) -> urlutils.URL: @@ -86,6 +90,9 @@ class DisplayMixin: size = ... # type: Column resolution_width = ... # type: Column resolution_height = ... # type: Column + refresh_rate = ... # type: Column + contrast_ratio = ... # type: Column + touchable = ... # type: Column def __init__(self) -> None: super().__init__() @@ -93,6 +100,9 @@ class DisplayMixin: self.size = ... # type: Integer self.resolution_width = ... # type: int self.resolution_height = ... # type: int + self.refresh_rate = ... # type: int + self.contrast_ratio = ... # type: int + self.touchable = ... # type: bool class Computer(DisplayMixin, Device): @@ -135,7 +145,11 @@ class Desktop(Computer): class Laptop(Computer): - pass + layout = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.layout = ... # type: Layouts class Server(Computer): @@ -233,12 +247,18 @@ class Motherboard(Component): self.pcmcia = ... # type: int -class NetworkAdapter(Component): +class NetworkMixin: speed = ... # type: Column + wireless = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.speed = ... # type: int + self.wireless = ... # type: bool + + +class NetworkAdapter(NetworkMixin, Component): + pass class Processor(Component): @@ -271,6 +291,88 @@ class Display(DisplayMixin, Component): pass +class ComputerAccessory(Device): + pass + + +class SAI(ComputerAccessory): + pass + + +class Keyboard(ComputerAccessory): + layout = ... # type: Column + + def __init__(self, layout: Layouts, **kwargs): + super().__init__(**kwargs) + self.layout = ... # type: Layouts + + +class Mouse(ComputerAccessory): + pass + + +class MemoryCardReader(ComputerAccessory): + pass + + +class Networking(NetworkMixin, Device): + pass + + +class Router(Networking): + pass + + +class Switch(Networking): + pass + + +class Hub(Networking): + pass + + +class WirelessAccessPoint(Networking): + pass + + +class Printer(Device): + wireless = ... # type: Column + scanning = ... # type: Column + technology = ... # type: Column + monochrome = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.wireless = ... # type: bool + self.scanning = ... # type: bool + self.technology = ... # type: PrinterTechnology + self.monochrome = ... # type: bool + + +class LabelPrinter(Printer): + pass + + +class Sound(Device): + pass + + +class Microphone(Sound): + pass + + +class Video(Device): + pass + + +class VideoScaler(Video): + pass + + +class Videoconference(Video): + pass + + class Manufacturer(Model): CUSTOM_MANUFACTURERS = ... # type: set name = ... # type: Column diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index db3f6a9a..c53621bd 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -1,15 +1,16 @@ from marshmallow import post_load, pre_load -from marshmallow.fields import Boolean, Float, Integer, List, Str, String +from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Str, String from marshmallow.validate import Length, OneOf, Range from sqlalchemy.util import OrderedSet from stdnum import imei, meid +from teal.enums import Layouts from teal.marshmallow import EnumField, SanitizedStr, URL, ValidationError from teal.resource import Schema from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.device import models as m, states from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \ - DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface + DataStoragePrivacyCompliance, DisplayTech, PrinterTechnology, RamFormat, RamInterface from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing, UnitCodes @@ -40,6 +41,9 @@ class Device(Thing): trading = EnumField(states.Trading, dump_only=True, description=m.Device.trading.__doc__) physical = EnumField(states.Physical, dump_only=True, description=m.Device.physical.__doc__) physical_possessor = NestedOn('Agent', dump_only=True, data_key='physicalPossessor') + production_date = DateTime('iso', + description=m.Device.updated.comment, + data_key='productionDate') @pre_load def from_events_to_events_one(self, data: dict): @@ -98,6 +102,9 @@ class DisplayMixin: resolution_height = Integer(data_key='resolutionHeight', validate=Range(10, 20000), description=m.DisplayMixin.resolution_height.comment) + refresh_rate = Integer(data_key='refreshRate', validate=Range(10, 1000)) + contrast_ratio = Integer(data_key='contrastRatio', validate=Range(100, 100000)) + touchable = Boolean(missing=False, description=m.DisplayMixin.touchable.comment) class NetworkMixin: @@ -212,3 +219,74 @@ class Manufacturer(Schema): name = String(dump_only=True) url = URL(dump_only=True) logo = URL(dump_only=True) + + +class ComputerAccessory(Device): + pass + + +class Mouse(ComputerAccessory): + pass + + +class MemoryCardReader(ComputerAccessory): + pass + + +class SAI(ComputerAccessory): + pass + + +class Keyboard(ComputerAccessory): + layout = EnumField(Layouts) + + +class Networking(NetworkMixin, Device): + pass + + +class Router(Networking): + pass + + +class Switch(Networking): + pass + + +class Hub(Networking): + pass + + +class WirelessAccessPoint(Networking): + pass + + +class Printer(Device): + wireless = Boolean(required=True, missing=False) + scanning = Boolean(required=True, missing=False) + technology = EnumField(PrinterTechnology, required=True) + monochrome = Boolean(required=True, missing=True) + + +class LabelPrinter(Printer): + pass + + +class Sound(Device): + pass + + +class Microphone(Sound): + pass + + +class Video(Device): + pass + + +class VideoScaler(Video): + pass + + +class Videoconference(Video): + pass diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py index de99d8f7..9b882f46 100644 --- a/ereuse_devicehub/resources/enums.py +++ b/ereuse_devicehub/resources/enums.py @@ -276,3 +276,12 @@ class DataStoragePrivacyCompliance(Enum): return cls.EraseSectors if not erasure.error else cls.EraseSectorsError else: return cls.EraseBasic if not erasure.error else cls.EraseBasicError + + +class PrinterTechnology(Enum): + """Technology of the printer.""" + Toner = 'Toner / Laser' + Inkjet = 'Liquid inkjet' + SolidInk = 'Solid ink' + Dye = 'Dye-sublimation' + Thermal = 'Thermal' diff --git a/ereuse_devicehub/resources/models.pyi b/ereuse_devicehub/resources/models.pyi index e1d59764..deb89a77 100644 --- a/ereuse_devicehub/resources/models.pyi +++ b/ereuse_devicehub/resources/models.pyi @@ -1,5 +1,6 @@ -from sqlalchemy import Column +from datetime import datetime +from sqlalchemy import Column from teal.db import Model STR_SIZE = 64 @@ -13,3 +14,8 @@ class Thing(Model): type = ... # type: str updated = ... # type: Column created = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.updated = ... # type: datetime + self.created = ... # type: datetime diff --git a/ereuse_devicehub/resources/schemas.py b/ereuse_devicehub/resources/schemas.py index 0aec435a..75652401 100644 --- a/ereuse_devicehub/resources/schemas.py +++ b/ereuse_devicehub/resources/schemas.py @@ -22,8 +22,8 @@ class UnitCodes(Enum): class Thing(Schema): type = String(description='Only required when it is nested.') same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs') - updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment.strip()) - created = DateTime('iso', dump_only=True, description=m.Thing.created.comment.strip()) + updated = DateTime('iso', dump_only=True, description=m.Thing.updated.comment) + created = DateTime('iso', dump_only=True, description=m.Thing.created.comment) @post_load def remove_type(self, data: dict): diff --git a/tests/test_basic.py b/tests/test_basic.py index f51a384d..2097967f 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -40,4 +40,4 @@ def test_api_docs(client: Client): 'scheme': 'basic', 'name': 'Authorization' } - assert 75 == len(docs['definitions']) + assert 92 == len(docs['definitions']) diff --git a/tests/test_device.py b/tests/test_device.py index 2c69620b..57af1d43 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -9,14 +9,14 @@ from ereuse_utils.test import ANY from pytest import raises from sqlalchemy.util import OrderedSet from teal.db import ResourceNotFound +from teal.enums import Layouts from ereuse_devicehub.client import Client, UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.agent.models import Person +from ereuse_devicehub.resources.device import models as d from ereuse_devicehub.resources.device.exceptions import NeedsId -from ereuse_devicehub.resources.device.models import Component, ComputerMonitor, DataStorage, \ - Desktop, Device, GraphicCard, Laptop, Motherboard, NetworkAdapter from ereuse_devicehub.resources.device.schemas import Device as DeviceS from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \ Sync @@ -34,43 +34,43 @@ def test_device_model(): """ Tests that the correctness of the device model and its relationships. """ - pc = Desktop(model='p1mo', - manufacturer='p1ma', - serial_number='p1s', - chassis=ComputerChassis.Tower) - net = NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s') - graphic = GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) + pc = d.Desktop(model='p1mo', + manufacturer='p1ma', + serial_number='p1s', + chassis=ComputerChassis.Tower) + net = d.NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s') + graphic = d.GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) pc.components.add(net) pc.components.add(graphic) db.session.add(pc) db.session.commit() - pc = Desktop.query.one() + pc = d.Desktop.query.one() assert pc.serial_number == 'p1s' assert pc.components == OrderedSet([net, graphic]) - network_adapter = NetworkAdapter.query.one() + network_adapter = d.NetworkAdapter.query.one() assert network_adapter.parent == pc # Removing a component from pc doesn't delete the component pc.components.remove(net) db.session.commit() - pc = Device.query.first() # this is the same as querying for Desktop directly + pc = d.Device.query.first() # this is the same as querying for d.Desktop directly assert pc.components == {graphic} - network_adapter = NetworkAdapter.query.one() + network_adapter = d.NetworkAdapter.query.one() assert network_adapter not in pc.components assert network_adapter.parent is None # Deleting the pc deletes everything - gcard = GraphicCard.query.one() + gcard = d.GraphicCard.query.one() db.session.delete(pc) db.session.flush() assert pc.id == 1 - assert Desktop.query.first() is None + assert d.Desktop.query.first() is None db.session.commit() - assert Desktop.query.first() is None + assert d.Desktop.query.first() is None assert network_adapter.id == 2 - assert NetworkAdapter.query.first() is not None, 'We removed the network adaptor' + assert d.NetworkAdapter.query.first() is not None, 'We removed the network adaptor' assert gcard.id == 3, 'We should still hold a reference to a zombie graphic card' - assert GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc' + assert d.GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc' @pytest.mark.usefixtures(conftest.app_context.__name__) @@ -78,26 +78,26 @@ def test_device_schema(): """Ensures the user does not upload non-writable or extra fields.""" device_s = DeviceS() device_s.load({'serialNumber': 'foo1', 'model': 'foo', 'manufacturer': 'bar2'}) - device_s.dump(Device(id=1)) + device_s.dump(d.Device(id=1)) @pytest.mark.usefixtures(conftest.app_context.__name__) def test_physical_properties(): - c = Motherboard(slots=2, - usb=3, - serial_number='sn', - model='ml', - manufacturer='mr', - width=2.0, - color=Color()) - pc = Desktop(chassis=ComputerChassis.Tower, - model='foo', - manufacturer='bar', - serial_number='foo-bar', - weight=2.8, - width=1.4, - height=2.1, - color=Color('LightSeaGreen')) + c = d.Motherboard(slots=2, + usb=3, + serial_number='sn', + model='ml', + manufacturer='mr', + width=2.0, + color=Color()) + pc = d.Desktop(chassis=ComputerChassis.Tower, + model='foo', + manufacturer='bar', + serial_number='foo-bar', + weight=2.8, + width=1.4, + height=2.1, + color=Color('LightSeaGreen')) pc.components.add(c) db.session.add(pc) db.session.commit() @@ -113,7 +113,6 @@ def test_physical_properties(): 'weight': None, 'height': None, 'width': 2.0, - 'color': Color(), 'depth': None } assert pc.physical_properties == { @@ -124,7 +123,6 @@ def test_physical_properties(): 'width': 1.4, 'height': 2.1, 'depth': None, - 'color': Color('LightSeaGreen'), 'chassis': ComputerChassis.Tower } @@ -132,18 +130,18 @@ def test_physical_properties(): @pytest.mark.usefixtures(conftest.app_context.__name__) def test_component_similar_one(): snapshot = conftest.file('pc-components.db') - d = snapshot['device'] + pc = snapshot['device'] snapshot['components'][0]['serial_number'] = snapshot['components'][1]['serial_number'] = None - pc = Desktop(**d, components=OrderedSet(Component(**c) for c in snapshot['components'])) - component1, component2 = pc.components # type: Component + pc = d.Desktop(**pc, components=OrderedSet(d.Component(**c) for c in snapshot['components'])) + component1, component2 = pc.components # type: d.Component db.session.add(pc) db.session.flush() # Let's create a new component named 'A' similar to 1 - componentA = Component(model=component1.model, manufacturer=component1.manufacturer) + componentA = d.Component(model=component1.model, manufacturer=component1.manufacturer) similar_to_a = componentA.similar_one(pc, set()) assert similar_to_a == component1 - # Component B does not have the same model - componentB = Component(model='nope', manufacturer=component1.manufacturer) + # d.Component B does not have the same model + componentB = d.Component(model='nope', manufacturer=component1.manufacturer) with pytest.raises(ResourceNotFound): assert componentB.similar_one(pc, set()) # If we blacklist component A we won't get anything @@ -159,14 +157,14 @@ def test_add_remove(): # c4 is not with any pc values = conftest.file('pc-components.db') pc = values['device'] - c1, c2 = (Component(**c) for c in values['components']) - pc = Desktop(**pc, components=OrderedSet([c1, c2])) + c1, c2 = (d.Component(**c) for c in values['components']) + pc = d.Desktop(**pc, components=OrderedSet([c1, c2])) db.session.add(pc) - c3 = Component(serial_number='nc1') - pc2 = Desktop(serial_number='s2', - components=OrderedSet([c3]), - chassis=ComputerChassis.Microtower) - c4 = Component(serial_number='c4s') + c3 = d.Component(serial_number='nc1') + pc2 = d.Desktop(serial_number='s2', + components=OrderedSet([c3]), + chassis=ComputerChassis.Microtower) + c4 = d.Component(serial_number='c4s') db.session.add(pc2) db.session.add(c4) db.session.commit() @@ -189,12 +187,12 @@ def test_sync_run_components_empty(): remove all the components from the device. """ s = conftest.file('pc-components.db') - pc = Desktop(**s['device'], components=OrderedSet(Component(**c) for c in s['components'])) + pc = d.Desktop(**s['device'], components=OrderedSet(d.Component(**c) for c in s['components'])) db.session.add(pc) db.session.commit() # Create a new transient non-db synced object - pc = Desktop(**s['device']) + pc = d.Desktop(**s['device']) db_pc, _ = Sync().run(pc, components=OrderedSet()) assert not db_pc.components assert not pc.components @@ -207,25 +205,25 @@ def test_sync_run_components_none(): keep all the components from the device. """ s = conftest.file('pc-components.db') - pc = Desktop(**s['device'], components=OrderedSet(Component(**c) for c in s['components'])) + pc = d.Desktop(**s['device'], components=OrderedSet(d.Component(**c) for c in s['components'])) db.session.add(pc) db.session.commit() # Create a new transient non-db synced object - transient_pc = Desktop(**s['device']) + transient_pc = d.Desktop(**s['device']) db_pc, _ = Sync().run(transient_pc, components=None) assert db_pc.components assert db_pc.components == pc.components @pytest.mark.usefixtures(conftest.app_context.__name__) -def test_sync_execute_register_desktop_new_Desktop_no_tag(): +def test_sync_execute_register_desktop_new_desktop_no_tag(): """ - Syncs a new Desktop with HID and without a tag, creating it. + Syncs a new d.Desktop with HID and without a tag, creating it. :return: """ # Case 1: device does not exist on DB - pc = Desktop(**conftest.file('pc-components.db')['device']) + pc = d.Desktop(**conftest.file('pc-components.db')['device']) db_pc = Sync().execute_register(pc) assert pc.physical_properties == db_pc.physical_properties @@ -233,13 +231,13 @@ def test_sync_execute_register_desktop_new_Desktop_no_tag(): @pytest.mark.usefixtures(conftest.app_context.__name__) def test_sync_execute_register_desktop_existing_no_tag(): """ - Syncs an existing Desktop with HID and without a tag. + Syncs an existing d.Desktop with HID and without a tag. """ - pc = Desktop(**conftest.file('pc-components.db')['device']) + pc = d.Desktop(**conftest.file('pc-components.db')['device']) db.session.add(pc) db.session.commit() - pc = Desktop( + pc = d.Desktop( **conftest.file('pc-components.db')['device']) # Create a new transient non-db object # 1: device exists on DB db_pc = Sync().execute_register(pc) @@ -249,11 +247,11 @@ def test_sync_execute_register_desktop_existing_no_tag(): @pytest.mark.usefixtures(conftest.app_context.__name__) def test_sync_execute_register_desktop_no_hid_no_tag(): """ - Syncs a Desktop without HID and no tag. + Syncs a d.Desktop without HID and no tag. This should fail as we don't have a way to identify it. """ - pc = Desktop(**conftest.file('pc-components.db')['device']) + pc = d.Desktop(**conftest.file('pc-components.db')['device']) # 1: device has no HID pc.hid = pc.model = None with pytest.raises(NeedsId): @@ -263,7 +261,7 @@ def test_sync_execute_register_desktop_no_hid_no_tag(): @pytest.mark.usefixtures(conftest.app_context.__name__) def test_sync_execute_register_desktop_tag_not_linked(): """ - Syncs a new Desktop with HID and a non-linked tag. + Syncs a new d.Desktop with HID and a non-linked tag. It is OK if the tag was not linked, it will be linked in this process. """ @@ -272,24 +270,24 @@ def test_sync_execute_register_desktop_tag_not_linked(): db.session.commit() # Create a new transient non-db object - pc = Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([Tag(id='foo')])) + pc = d.Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([Tag(id='foo')])) returned_pc = Sync().execute_register(pc) assert returned_pc == pc assert tag.device == pc, 'Tag has to be linked' - assert Desktop.query.one() == pc, 'Desktop had to be set to db' + assert d.Desktop.query.one() == pc, 'd.Desktop had to be set to db' @pytest.mark.usefixtures(conftest.app_context.__name__) def test_sync_execute_register_no_hid_tag_not_linked(tag_id: str): """ - Validates registering a Desktop without HID and a non-linked tag. + Validates registering a d.Desktop without HID and a non-linked tag. In this case it is ok still, as the non-linked tag proves that - the Desktop was not existing before (otherwise the tag would - be linked), and thus it creates a new Desktop. + the d.Desktop was not existing before (otherwise the tag would + be linked), and thus it creates a new d.Desktop. """ tag = Tag(id=tag_id) - pc = Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([tag])) + pc = d.Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([tag])) returned_pc = Sync().execute_register(pc) db.session.commit() assert returned_pc == pc @@ -299,7 +297,7 @@ def test_sync_execute_register_no_hid_tag_not_linked(tag_id: str): # they have the same pk though assert tag != db_tag, 'They are not the same tags though' assert db_tag.id == tag.id - assert Desktop.query.one() == pc, 'Desktop had to be set to db' + assert d.Desktop.query.one() == pc, 'd.Desktop had to be set to db' @pytest.mark.usefixtures(conftest.app_context.__name__) @@ -310,7 +308,7 @@ def test_sync_execute_register_tag_does_not_exist(): Tags have to be created before trying to link them through a Snapshot. """ - pc = Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([Tag('foo')])) + pc = d.Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([Tag('foo')])) with raises(ResourceNotFound): Sync().execute_register(pc) @@ -323,11 +321,11 @@ def test_sync_execute_register_tag_linked_same_device(): (If it has HID it validates both HID and tag point at the same device, this his checked in ). """ - orig_pc = Desktop(**conftest.file('pc-components.db')['device']) + orig_pc = d.Desktop(**conftest.file('pc-components.db')['device']) db.session.add(Tag(id='foo', device=orig_pc)) db.session.commit() - pc = Desktop( + pc = d.Desktop( **conftest.file('pc-components.db')['device']) # Create a new transient non-db object pc.tags.add(Tag(id='foo')) db_pc = Sync().execute_register(pc) @@ -342,15 +340,15 @@ def test_sync_execute_register_tag_linked_other_device_mismatch_between_tags(): Checks that sync raises an error if finds that at least two passed-in tags are not linked to the same device. """ - pc1 = Desktop(**conftest.file('pc-components.db')['device']) + pc1 = d.Desktop(**conftest.file('pc-components.db')['device']) db.session.add(Tag(id='foo-1', device=pc1)) - pc2 = Desktop(**conftest.file('pc-components.db')['device']) + pc2 = d.Desktop(**conftest.file('pc-components.db')['device']) pc2.serial_number = 'pc2-serial' pc2.hid = Naming.hid(pc2.manufacturer, pc2.serial_number, pc2.model) db.session.add(Tag(id='foo-2', device=pc2)) db.session.commit() - pc1 = Desktop( + pc1 = d.Desktop( **conftest.file('pc-components.db')['device']) # Create a new transient non-db object pc1.tags.add(Tag(id='foo-1')) pc1.tags.add(Tag(id='foo-2')) @@ -366,15 +364,15 @@ def test_sync_execute_register_mismatch_between_tags_and_hid(): In this case we set HID -> pc1 but tag -> pc2 """ - pc1 = Desktop(**conftest.file('pc-components.db')['device']) + pc1 = d.Desktop(**conftest.file('pc-components.db')['device']) db.session.add(Tag(id='foo-1', device=pc1)) - pc2 = Desktop(**conftest.file('pc-components.db')['device']) + pc2 = d.Desktop(**conftest.file('pc-components.db')['device']) pc2.serial_number = 'pc2-serial' pc2.hid = Naming.hid(pc2.manufacturer, pc2.serial_number, pc2.model) db.session.add(Tag(id='foo-2', device=pc2)) db.session.commit() - pc1 = Desktop( + pc1 = d.Desktop( **conftest.file('pc-components.db')['device']) # Create a new transient non-db object pc1.tags.add(Tag(id='foo-2')) with raises(MismatchBetweenTagsAndHid): @@ -382,15 +380,15 @@ def test_sync_execute_register_mismatch_between_tags_and_hid(): def test_get_device(app: Devicehub, user: UserClient): - """Checks GETting a Desktop with its components.""" + """Checks GETting a d.Desktop with its components.""" with app.app_context(): - pc = Desktop(model='p1mo', - manufacturer='p1ma', - serial_number='p1s', - chassis=ComputerChassis.Tower) + pc = d.Desktop(model='p1mo', + manufacturer='p1ma', + serial_number='p1s', + chassis=ComputerChassis.Tower) pc.components = OrderedSet([ - NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), - GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) + d.NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), + d.GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) ]) db.session.add(pc) db.session.add(Test(device=pc, @@ -399,7 +397,7 @@ def test_get_device(app: Devicehub, user: UserClient): agent=Person(name='Timmy'), author=User(email='bar@bar.com'))) db.session.commit() - pc, _ = user.get(res=Device, item=1) + pc, _ = user.get(res=d.Device, item=1) assert len(pc['events']) == 1 assert pc['events'][0]['type'] == 'Test' assert pc['events'][0]['device'] == 1 @@ -414,46 +412,46 @@ def test_get_device(app: Devicehub, user: UserClient): assert pc['model'] == 'p1mo' assert pc['manufacturer'] == 'p1ma' assert pc['serialNumber'] == 'p1s' - assert pc['type'] == 'Desktop' + assert pc['type'] == d.Desktop.t def test_get_devices(app: Devicehub, user: UserClient): """Checks GETting multiple devices.""" with app.app_context(): - pc = Desktop(model='p1mo', - manufacturer='p1ma', - serial_number='p1s', - chassis=ComputerChassis.Tower) + pc = d.Desktop(model='p1mo', + manufacturer='p1ma', + serial_number='p1s', + chassis=ComputerChassis.Tower) pc.components = OrderedSet([ - NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), - GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) + d.NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), + d.GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) ]) - pc1 = Desktop(model='p2mo', - manufacturer='p2ma', - serial_number='p2s', - chassis=ComputerChassis.Tower) - pc2 = Laptop(model='p3mo', - manufacturer='p3ma', - serial_number='p3s', - chassis=ComputerChassis.Netbook) + pc1 = d.Desktop(model='p2mo', + manufacturer='p2ma', + serial_number='p2s', + chassis=ComputerChassis.Tower) + pc2 = d.Laptop(model='p3mo', + manufacturer='p3ma', + serial_number='p3s', + chassis=ComputerChassis.Netbook) db.session.add_all((pc, pc1, pc2)) db.session.commit() - devices, _ = user.get(res=Device) - assert tuple(d['id'] for d in devices['items']) == (1, 2, 3, 4, 5) - assert tuple(d['type'] for d in devices['items']) == ( - 'Desktop', 'Desktop', 'Laptop', 'NetworkAdapter', 'GraphicCard' + devices, _ = user.get(res=d.Device) + assert tuple(dev['id'] for dev in devices['items']) == (1, 2, 3, 4, 5) + assert tuple(dev['type'] for dev in devices['items']) == ( + d.Desktop.t, d.Desktop.t, d.Laptop.t, d.NetworkAdapter.t, d.GraphicCard.t ) @pytest.mark.usefixtures(conftest.app_context.__name__) def test_computer_monitor(): - m = ComputerMonitor(technology=DisplayTech.LCD, - manufacturer='foo', - model='bar', - serial_number='foo-bar', - resolution_width=1920, - resolution_height=1080, - size=14.5) + m = d.ComputerMonitor(technology=DisplayTech.LCD, + manufacturer='foo', + model='bar', + serial_number='foo-bar', + resolution_width=1920, + resolution_height=1080, + size=14.5) db.session.add(m) db.session.commit() @@ -489,7 +487,7 @@ def test_manufacturer_enforced(): def test_device_properties_format(app: Devicehub, user: UserClient): user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot) with app.app_context(): - pc = Laptop.query.one() # type: Laptop + pc = d.Laptop.query.one() # type: d.Laptop assert format(pc) == 'Laptop 1: model 1000h, S/N 94oaaq021116' assert format(pc, 't') == 'Netbook 1000h' assert format(pc, 's') == '(asustek computer inc.) S/N 94OAAQ021116' @@ -497,12 +495,12 @@ def test_device_properties_format(app: Devicehub, user: UserClient): assert pc.data_storage_size == 152627 assert pc.graphic_card_model == 'mobile 945gse express integrated graphics controller' assert pc.processor_model == 'intel atom cpu n270 @ 1.60ghz' - net = next(c for c in pc.components if isinstance(c, NetworkAdapter)) + net = next(c for c in pc.components if isinstance(c, d.NetworkAdapter)) assert format(net) == 'NetworkAdapter 2: model ar8121/ar8113/ar8114 ' \ 'gigabit or fast ethernet, S/N 00:24:8c:7f:cf:2d' assert format(net, 't') == 'NetworkAdapter ar8121/ar8113/ar8114 gigabit or fast ethernet' assert format(net, 's') == '(qualcomm atheros) S/N 00:24:8C:7F:CF:2D – 100 Mbps' - hdd = next(c for c in pc.components if isinstance(c, DataStorage)) + hdd = next(c for c in pc.components if isinstance(c, d.DataStorage)) assert format(hdd) == 'HardDrive 7: model st9160310as, S/N 5sv4tqa6' assert format(hdd, 't') == 'HardDrive st9160310as' assert format(hdd, 's') == '(seagate) S/N 5SV4TQA6 – 152 GB' @@ -510,6 +508,26 @@ def test_device_properties_format(app: Devicehub, user: UserClient): def test_device_public(user: UserClient, client: Client): s, _ = user.post(file('asus-eee-1000h.snapshot.11'), res=m.Snapshot) - html, _ = client.get(res=Device, item=s['device']['id'], accept=ANY) + html, _ = client.get(res=d.Device, item=s['device']['id'], accept=ANY) assert 'intel atom cpu n270 @ 1.60ghz' in html assert 'S/N 00:24:8C:7F:CF:2D – 100 Mbps' in html + + +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_computer_accessory_model(): + sai = d.SAI() + db.session.add(sai) + keyboard = d.Keyboard(layout=Layouts.ES) + db.session.add(keyboard) + mouse = d.Mouse() + db.session.add(mouse) + db.session.commit() + + +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_networking_model(): + router = d.Router(speed=1000, wireless=True) + db.session.add(router) + switch = d.Switch(speed=1000, wireless=False) + db.session.add(switch) + db.session.commit() diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 85ff8697..afdaa1e9 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -411,3 +411,10 @@ def snapshot_and_check(user: UserClient, return snapshot_and_check(user, input_snapshot, event_types, perform_second_snapshot=False) else: return snapshot + + +def test_snapshot_keyboard(user: UserClient): + s = file('keyboard.snapshot') + snapshot = snapshot_and_check(user, s, event_types=('ManualRate',)) + keyboard = snapshot['device'] + assert keyboard['layout'] == 'ES' From 3978e606fd70438ecd92ad1c53b2dd6607741bf1 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Tue, 23 Oct 2018 16:24:26 +0200 Subject: [PATCH 6/7] Add rates to dummy computers --- .../files/asus-eee-1000h.snapshot.11.yaml | 6 +++++ .../files/dell-optiplexgx520.snapshot.11.yaml | 6 +++++ .../dummy/files/nec.snapshot.11.yaml | 6 +++++ .../files/real-eee-1001pxd.snapshot.11.yaml | 6 +++++ .../dummy/files/real-hp.snapshot.11.yaml | 6 +++++ tests/test_workbench.py | 22 +++++++++++++++++-- 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/ereuse_devicehub/dummy/files/asus-eee-1000h.snapshot.11.yaml b/ereuse_devicehub/dummy/files/asus-eee-1000h.snapshot.11.yaml index aeda0cc3..5a07c844 100644 --- a/ereuse_devicehub/dummy/files/asus-eee-1000h.snapshot.11.yaml +++ b/ereuse_devicehub/dummy/files/asus-eee-1000h.snapshot.11.yaml @@ -17,6 +17,12 @@ "elapsed": 19, "rate": 19.3106, "type": "BenchmarkRamSysbench" + }, + { + "appearanceRange": "A", + "biosRange": "A", + "functionalityRange": "A", + "type": "WorkbenchRate" } ], "manufacturer": "ASUSTeK Computer INC." diff --git a/ereuse_devicehub/dummy/files/dell-optiplexgx520.snapshot.11.yaml b/ereuse_devicehub/dummy/files/dell-optiplexgx520.snapshot.11.yaml index 2769489e..756e065a 100644 --- a/ereuse_devicehub/dummy/files/dell-optiplexgx520.snapshot.11.yaml +++ b/ereuse_devicehub/dummy/files/dell-optiplexgx520.snapshot.11.yaml @@ -12,6 +12,12 @@ "error": false, "type": "StressTest", "elapsed": 60 + }, + { + "appearanceRange": "A", + "biosRange": "A", + "functionalityRange": "A", + "type": "WorkbenchRate" } ], "type": "Desktop", diff --git a/ereuse_devicehub/dummy/files/nec.snapshot.11.yaml b/ereuse_devicehub/dummy/files/nec.snapshot.11.yaml index 54c645ea..3ad7629f 100644 --- a/ereuse_devicehub/dummy/files/nec.snapshot.11.yaml +++ b/ereuse_devicehub/dummy/files/nec.snapshot.11.yaml @@ -14,6 +14,12 @@ "rate": 0.9323, "elapsed": 1, "type": "BenchmarkRamSysbench" + }, + { + "appearanceRange": "B", + "biosRange": "A", + "functionalityRange": "C", + "type": "WorkbenchRate" } ], "type": "Desktop", diff --git a/ereuse_devicehub/dummy/files/real-eee-1001pxd.snapshot.11.yaml b/ereuse_devicehub/dummy/files/real-eee-1001pxd.snapshot.11.yaml index 47bf9d8f..70d838e2 100644 --- a/ereuse_devicehub/dummy/files/real-eee-1001pxd.snapshot.11.yaml +++ b/ereuse_devicehub/dummy/files/real-eee-1001pxd.snapshot.11.yaml @@ -145,6 +145,12 @@ "type": "StressTest", "error": false, "elapsed": 60 + }, + { + "appearanceRange": "B", + "biosRange": "C", + "functionalityRange": "A", + "type": "WorkbenchRate" } ] }, diff --git a/ereuse_devicehub/dummy/files/real-hp.snapshot.11.yaml b/ereuse_devicehub/dummy/files/real-hp.snapshot.11.yaml index 30b3e122..7288f5e0 100644 --- a/ereuse_devicehub/dummy/files/real-hp.snapshot.11.yaml +++ b/ereuse_devicehub/dummy/files/real-hp.snapshot.11.yaml @@ -148,6 +148,12 @@ "rate": 0.9759, "type": "BenchmarkRamSysbench", "elapsed": 1 + }, + { + "appearanceRange": "B", + "biosRange": "A", + "functionalityRange": "D", + "type": "WorkbenchRate" } ], "serialNumber": "CZC0408YJG", diff --git a/tests/test_workbench.py b/tests/test_workbench.py index f835ce21..1b1b7f2a 100644 --- a/tests/test_workbench.py +++ b/tests/test_workbench.py @@ -142,6 +142,9 @@ def test_real_hp_11(user: UserClient): assert pc['hid'] == 'hewlett-packard-czc0408yjg-hp_compaq_8100_elite_sff' assert pc['chassis'] == 'Tower' assert set(e['type'] for e in snapshot['events']) == { + 'EreusePrice', + 'AggregateRate', + 'WorkbenchRate', 'BenchmarkDataStorage', 'BenchmarkProcessor', 'BenchmarkProcessorSysbench', @@ -149,11 +152,12 @@ def test_real_hp_11(user: UserClient): 'BenchmarkRamSysbench', 'StressTest' } - assert len(list(e['type'] for e in snapshot['events'])) == 6 + assert len(list(e['type'] for e in snapshot['events'])) == 9 assert pc['networkSpeeds'] == [1000, None], 'Device has no WiFi' assert pc['processorModel'] == 'intel core i3 cpu 530 @ 2.93ghz' assert pc['ramSize'] == 8192 assert pc['dataStorageSize'] == 305245 + # todo check rating def test_real_toshiba_11(user: UserClient): @@ -177,6 +181,20 @@ def test_snapshot_real_eee_1001pxd(user: UserClient): assert pc['hid'] == 'asustek_computer_inc-b8oaas048286-1001pxd' assert pc['tags'] == [] assert pc['networkSpeeds'] == [100, 0], 'Although it has WiFi we do not know the speed' + assert pc['rate'] + rate = pc['rate'] + assert rate['appearanceRange'] == 'B' + assert rate['functionalityRange'] == 'A' + assert rate['processorRange'] == 'VERY_LOW' + assert rate['ramRange'] == 'VERY_LOW' + assert rate['ratingRange'] == 'VERY_LOW' + assert rate['ram'] == 1.53 + assert rate['data_storage'] == 3.76 + assert rate['type'] == 'AggregateRate' + assert rate['biosRange'] == 'C' + assert rate['appearance'] > 0 + assert rate['functionality'] > 0 + assert rate['rating'] > 0 and rate['rating'] != 1 components = snapshot['components'] wifi = components[0] assert wifi['hid'] == 'qualcomm_atheros-74_2f_68_8b_fd_c8-ar9285_wireless_network_adapter' @@ -208,7 +226,7 @@ def test_snapshot_real_eee_1001pxd(user: UserClient): assert em.BenchmarkRamSysbench.t in event_types assert em.StressTest.t in event_types assert em.Snapshot.t in event_types - assert len(events) == 5 + assert len(events) == 7 gpu = components[3] assert gpu['model'] == 'atom processor d4xx/d5xx/n4xx/n5xx integrated graphics controller' assert gpu['manufacturer'] == 'intel corporation' From 6abc11b40c1229cf8c95b9d311249a4e03af40c1 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Tue, 23 Oct 2018 16:24:50 +0200 Subject: [PATCH 7/7] Add keyboard fixture --- tests/files/keyboard.snapshot.yaml | 1 + 1 file changed, 1 insertion(+) create mode 120000 tests/files/keyboard.snapshot.yaml diff --git a/tests/files/keyboard.snapshot.yaml b/tests/files/keyboard.snapshot.yaml new file mode 120000 index 00000000..22f83385 --- /dev/null +++ b/tests/files/keyboard.snapshot.yaml @@ -0,0 +1 @@ +../../ereuse_devicehub/dummy/files/keyboard.snapshot.yaml \ No newline at end of file