From 10c73a4e75ec80ddfb7e3ce6e23b8dc22e2366c3 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Tue, 23 Oct 2018 15:37:37 +0200 Subject: [PATCH] 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'