diff --git a/README.md b/README.md index d69e4380..bdb992e2 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,23 @@ $ sudo -u postgres -i postgres$ createdb dh-db1 ``` +Then execute, in the same directory where `app.py` is: +```bash +$ flask init-db +``` +This creates the tables in the database you created before. -And then execute, in the same directory where `app.py` is: +Finally, run the app: ```bash $ flask run ``` + See the [Flask quickstart](http://flask.pocoo.org/docs/1.0/quickstart/) for more info. + +Devicehub has many commands that allows you to administrate it. You +can, for example, create a dummy database of devices with ``flask dummy`` +or create users with ``flask create-user``. See all the +available commands by just executing ``flask``. \ No newline at end of file diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index e796ebca..c6dce475 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -1,14 +1,14 @@ from distutils.version import StrictVersion from typing import Set -from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DataStorageDef, \ - DesktopDef, DeviceDef, GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, \ +from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, ComputerMonitorDef, \ + DataStorageDef, DesktopDef, DeviceDef, GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, \ MotherboardDef, NetbookDef, NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef, \ SolidStateDriveDef -from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, BenchmarkDataStorageDef, \ - BenchmarkDef, BenchmarkProcessorDef, BenchmarkProcessorSysbenchDef, BenchmarkRamSysbenchDef, \ - BenchmarkWithRateDef, EraseBasicDef, EraseSectorsDef, EventDef, InstallDef, \ - PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \ +from ereuse_devicehub.resources.event import AddDef, AggregateRateDef, AppRateDef, \ + BenchmarkDataStorageDef, BenchmarkDef, BenchmarkProcessorDef, BenchmarkProcessorSysbenchDef, \ + BenchmarkRamSysbenchDef, BenchmarkWithRateDef, EraseBasicDef, EraseSectorsDef, EventDef, \ + InstallDef, PhotoboxSystemRateDef, PhotoboxUserDef, RateDef, RemoveDef, SnapshotDef, StepDef, \ StepRandomDef, StepZeroDef, StressTestDef, TestDataStorageDef, TestDef, WorkbenchRateDef from ereuse_devicehub.resources.inventory import InventoryDef from ereuse_devicehub.resources.tag import TagDef @@ -19,13 +19,14 @@ from teal.config import Config class DevicehubConfig(Config): RESOURCE_DEFINITIONS = { DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, - MicrotowerDef, ComponentDef, GraphicCardDef, DataStorageDef, SolidStateDriveDef, + MicrotowerDef, ComputerMonitorDef, ComponentDef, GraphicCardDef, DataStorageDef, + SolidStateDriveDef, HardDriveDef, MotherboardDef, NetworkAdapterDef, RamModuleDef, ProcessorDef, UserDef, OrganizationDef, TagDef, EventDef, AddDef, RemoveDef, EraseBasicDef, EraseSectorsDef, StepDef, StepZeroDef, StepRandomDef, RateDef, AggregateRateDef, WorkbenchRateDef, PhotoboxUserDef, PhotoboxSystemRateDef, InstallDef, SnapshotDef, TestDef, TestDataStorageDef, StressTestDef, WorkbenchRateDef, InventoryDef, BenchmarkDef, - BenchmarkDataStorageDef, BenchmarkWithRateDef, BenchmarkProcessorDef, + BenchmarkDataStorageDef, BenchmarkWithRateDef, AppRateDef, BenchmarkProcessorDef, BenchmarkProcessorSysbenchDef, BenchmarkRamSysbenchDef } PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str] diff --git a/ereuse_devicehub/devicehub.py b/ereuse_devicehub/devicehub.py index ac13a817..a99cc8e3 100644 --- a/ereuse_devicehub/devicehub.py +++ b/ereuse_devicehub/devicehub.py @@ -5,12 +5,14 @@ from flask_sqlalchemy import SQLAlchemy from ereuse_devicehub.auth import Auth from ereuse_devicehub.client import Client from ereuse_devicehub.db import db +from ereuse_devicehub.dummy.dummy import Dummy from teal.config import Config as ConfigClass from teal.teal import Teal class Devicehub(Teal): test_client_class = Client + Dummy = Dummy def __init__(self, config: ConfigClass, @@ -29,5 +31,4 @@ class Devicehub(Teal): super().__init__(config, db, import_name, static_url_path, static_folder, static_host, host_matching, subdomain_matching, template_folder, instance_path, instance_relative_config, root_path, Auth) - - + self.dummy = Dummy(self) diff --git a/ereuse_devicehub/dummy/__init__.py b/ereuse_devicehub/dummy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/dummy/dummy.py b/ereuse_devicehub/dummy/dummy.py new file mode 100644 index 00000000..0963d00e --- /dev/null +++ b/ereuse_devicehub/dummy/dummy.py @@ -0,0 +1,62 @@ +from pathlib import Path + +import click +import click_spinner +import yaml +from tqdm import tqdm + +from ereuse_devicehub.client import UserClient +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.event.models import Snapshot +from ereuse_devicehub.resources.tag.model import Tag +from ereuse_devicehub.resources.user import User + + +class Dummy: + SNAPSHOTS = ( + 'workbench-server-1', + 'computer-monitor' + ) + TAGS = ( + 'tag1', + 'tag2', + 'tag3' + ) + + def __init__(self, app) -> None: + super().__init__() + self.app = app + self.app.cli.command('dummy', + short_help='Creates dummy devices and users.')(self.run) + + @click.confirmation_option(prompt='This command deletes the DB in the process. ' + 'Do you want to continue?') + def run(self): + print('Preparing the database...') + with click_spinner.spinner(): + self.app.init_db(erase=True) + user = self.user_client('user@dhub.com', '1234') + user.post(res=Tag, query=[('ids', i) for i in self.TAGS], data={}) + print('Creating devices...') + for file_name in tqdm(self.SNAPSHOTS): + snapshot = self.file(file_name) + user.post(res=Snapshot, data=snapshot) + print('Done :-)') + + def user_client(self, email: str, password: str): + user = User(email=email, password=password) + db.session.add(user) + db.session.commit() + client = UserClient(application=self.app, + response_wrapper=self.app.response_class, + email=user.email, + password=password) + client.user, _ = client.login(client.email, client.password) + return client + + def file(self, name: str): + with Path(__file__) \ + .parent \ + .joinpath('files') \ + .joinpath(name + '.snapshot.yaml').open() as f: + return yaml.load(f) diff --git a/ereuse_devicehub/dummy/files/computer-monitor.snapshot.yaml b/ereuse_devicehub/dummy/files/computer-monitor.snapshot.yaml new file mode 100644 index 00000000..aefc5817 --- /dev/null +++ b/ereuse_devicehub/dummy/files/computer-monitor.snapshot.yaml @@ -0,0 +1,17 @@ +type: Snapshot +software: AndroidApp +version: '1.0' +device: + type: ComputerMonitor + technology: LCD + manufacturer: Dell + model: 1707FPF + serialNumber: CN0FP446728728541C8S + resolutionWidth: 1920 + resolutionHeight: 1080 + size: 21.5 + events: + - type: AppRate + appearanceRange: A + functionalityRange: C + labelling: False diff --git a/ereuse_devicehub/dummy/files/workbench-server-1.snapshot.yaml b/ereuse_devicehub/dummy/files/workbench-server-1.snapshot.yaml new file mode 100644 index 00000000..015739b4 --- /dev/null +++ b/ereuse_devicehub/dummy/files/workbench-server-1.snapshot.yaml @@ -0,0 +1,118 @@ +# A Snapshot Phase 1 with a device +# and 1 GraphicCard, 2 RamModule, 1 Processor, 1 SSD, 1 HDD, 1 Motherboard +# Prerequisites: +# - 2 tags: tag1 and tag2 from the default org +# All numbers are invented + + +type: Snapshot +uuid: cb8ce6b5-6a1b-4084-b5b9-d8fadad2a015 +version: '11.0' +software: Workbench +elapsed: 500 +device: + type: Microtower + serialNumber: d1s + model: d1ml + manufacturer: d1mr + tags: + - type: Tag + id: tag1 + events: + - type: WorkbenchRate + appearanceRange: A + functionalityRange: B + - type: BenchmarkRamSysbench + rate: 2444 + - type: StressTest + elapsed: 300 + error: False +components: + - type: GraphicCard + serialNumber: gc1-1s + model: gc1-1ml + manufacturer: gc1-1mr + - type: RamModule + serialNumber: rm1-1s + model: rm1-1ml + manufacturer: rm1-1mr + - type: RamModule + serialNumber: rm2-1s + model: rm2-1ml + manufacturer: rm2-1mr + - type: Processor + model: p1-1s + manufacturer: p1-1mr + events: + - type: BenchmarkProcessor + rate: 2410 + - type: BenchmarkProcessorSysbench + rate: 4400 + - type: SolidStateDrive + serialNumber: ssd1-1s + model: ssd1-1ml + manufacturer: ssd1-1mr + events: + - type: BenchmarkDataStorage + readSpeed: 20 + writeSpeed: 15 + elapsed: 21 + - type: TestDataStorage + elapsed: 233 + firstError: 0 + error: False + status: Completed without error + length: Short + lifetime: 99 + passedLifetime: 99 + assessment: True + powerCycleCount: 11 + reallocatedSectorCount: 2 + powerCycleCount: 4 + reportedUncorrectableErrors: 1 + commandTimeout: 11 + currentPendingSectorCount: 1 + offlineUncorrectable: 33 + remainingLifetimePercentage: 1 + - type: EraseSectors + error: False + cleanWithZeros: False + startTime: 2018-01-01T10:10:10 + endTime: 2018-01-01T12:10:10 + secureRandomSteps: 0 + steps: + - type: StepRandom + startTime: 2018-01-01T10:10:10 + endTime: 2018-01-01T12:10:10 + error: False + cleanWithZeros: False + secureRandomSteps: 0 + - type: HardDrive + serialNumber: hdd1-1s + model: hdd1-1ml + manufacturer: hdd1-1mr + events: + - type: BenchmarkDataStorage + readSpeed: 10 + writeSpeed: 5 + - type: EraseSectors + error: False + cleanWithZeros: False + startTime: 2018-01-01T10:10:10 + endTime: 2018-01-01T12:10:10 + secureRandomSteps: 0 + steps: + - type: StepRandom + startTime: 2018-01-01T10:10:10 + endTime: 2018-01-01T12:10:10 + error: False + cleanWithZeros: False + secureRandomSteps: 0 + - type: Install + elapsed: 420 + error: False + name: LinuxMint 18.01 32b + - type: Motherboard + serialNumber: mb1-1s + model: mb1-1ml + manufacturer: mb1-1mr diff --git a/ereuse_devicehub/resources/device/__init__.py b/ereuse_devicehub/resources/device/__init__.py index 737a9329..0689629a 100644 --- a/ereuse_devicehub/resources/device/__init__.py +++ b/ereuse_devicehub/resources/device/__init__.py @@ -1,6 +1,6 @@ -from ereuse_devicehub.resources.device.schemas import Component, Computer, DataStorage, Desktop, \ - Device, GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, \ - Processor, RamModule, Server, SolidStateDrive +from ereuse_devicehub.resources.device.schemas import Component, Computer, ComputerMonitor, \ + DataStorage, Desktop, Device, GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, \ + NetworkAdapter, Processor, RamModule, Server, SolidStateDrive from ereuse_devicehub.resources.device.views import DeviceView from teal.resource import Converters, Resource @@ -36,6 +36,10 @@ class MicrotowerDef(ComputerDef): SCHEMA = Microtower +class ComputerMonitorDef(DeviceDef): + SCHEMA = ComputerMonitor + + class ComponentDef(DeviceDef): SCHEMA = Component diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 7cd1e067..6762caca 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -10,7 +10,8 @@ from sqlalchemy.orm import ColumnProperty, backref, relationship from sqlalchemy.util import OrderedSet from sqlalchemy_utils import ColorType -from ereuse_devicehub.resources.enums import DataStorageInterface, RamFormat, RamInterface +from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, DataStorageInterface, \ + RamFormat, RamInterface from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing from ereuse_utils.naming import Naming from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, check_range @@ -18,14 +19,30 @@ from teal.db import CASCADE, POLYMORPHIC_ID, POLYMORPHIC_ON, ResourceNotFound, c class Device(Thing): id = Column(BigInteger, Sequence('device_seq'), primary_key=True) + id.comment = """ + The identifier of the device for this database. + """ type = Column(Unicode(STR_SM_SIZE), nullable=False) hid = Column(Unicode(STR_BIG_SIZE), unique=True) + hid.comment = """ + The Hardware ID (HID) is the unique ID traceability systems + use to ID a device globally. + """ model = Column(Unicode(STR_BIG_SIZE)) manufacturer = Column(Unicode(STR_SIZE)) serial_number = Column(Unicode(STR_SIZE)) weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 3)) + weight.comment = """ + The weight of the device in Kgm. + """ width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 3)) + width.comment = """ + The width of the device in meters. + """ height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 3)) + height.comment = """ + The height of the device in meters. + """ depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3)) color = Column(ColorType) @@ -108,6 +125,28 @@ class Microtower(Computer): pass +class ComputerMonitor(Device): + id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) + size = Column(Float(decimal_return_scale=2), check_range('size', 2, 150)) + size.comment = """ + The size of the monitor in inches. + """ + technology = Column(DBEnum(ComputerMonitorTechnologies)) + technology.comment = """ + The technology the monitor uses to display the image. + """ + resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000)) + resolution_width.comment = """ + The maximum horizontal resolution the monitor can natively support + in pixels. + """ + resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000)) + resolution_height.comment = """ + The maximum vertical resolution the monitor can natively support + in pixels. + """ + + class Component(Device): id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi index bac400f5..b65a08a0 100644 --- a/ereuse_devicehub/resources/device/models.pyi +++ b/ereuse_devicehub/resources/device/models.pyi @@ -1,9 +1,10 @@ from typing import Dict, List, Set from colour import Color -from sqlalchemy import Column +from sqlalchemy import Column, Integer -from ereuse_devicehub.resources.enums import DataStorageInterface, RamFormat, RamInterface +from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, DataStorageInterface, \ + RamFormat, RamInterface from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \ EventWithOneDevice from ereuse_devicehub.resources.image.models import ImageList @@ -72,6 +73,20 @@ class Microtower(Computer): pass +class ComputerMonitor(Device): + technology = ... # type: Column + size = ... # type: Column + resolution_width = ... # type: Column + resolution_height = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + technology = ... # type: ComputerMonitorTechnologies + size = ... # type: Integer + resolution_width = ... # type: int + resolution_height = ... # type: int + + class Component(Device): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 020be4f8..831d33a4 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -1,36 +1,28 @@ -from ereuse_devicehub.marshmallow import NestedOn -from ereuse_devicehub.resources.enums import RamFormat, RamInterface -from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE -from ereuse_devicehub.resources.schemas import Thing, UnitCodes from marshmallow import post_load, pre_load from marshmallow.fields import Float, Integer, Str from marshmallow.validate import Length, OneOf, Range from marshmallow_enum import EnumField from sqlalchemy.util import OrderedSet +from ereuse_devicehub.marshmallow import NestedOn +from ereuse_devicehub.resources.device import models as m +from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies, RamFormat, RamInterface +from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE +from ereuse_devicehub.resources.schemas import Thing, UnitCodes from teal.marshmallow import ValidationError class Device(Thing): - # todo id is dump_only except when in Snapshot - id = Integer(description='The identifier of the device for this database.') - hid = Str(dump_only=True, - description='The Hardware ID is the unique ID traceability systems ' - 'use to ID a device globally.') + id = Integer(description=m.Device.id, dump_only=True) + hid = Str(dump_only=True, description=m.Device.hid) tags = NestedOn('Tag', many=True, collection_class=OrderedSet) model = Str(validate=Length(max=STR_BIG_SIZE)) manufacturer = Str(validate=Length(max=STR_SIZE)) serial_number = Str(data_key='serialNumber') product_id = Str(data_key='productId') - weight = Float(validate=Range(0.1, 3), - unit=UnitCodes.kgm, - description='The weight of the device in Kgm.') - width = Float(validate=Range(0.1, 3), - unit=UnitCodes.m, - description='The width of the device in meters.') - height = Float(validate=Range(0.1, 3), - unit=UnitCodes.m, - description='The height of the device in meters.') + weight = Float(validate=Range(0.1, 3), unit=UnitCodes.kgm, description=m.Device.weight) + width = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.width) + height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.height) events = NestedOn('Event', many=True, dump_only=True) events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) @@ -61,7 +53,6 @@ class Device(Thing): class Computer(Device): components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet) - pass class Desktop(Computer): @@ -84,6 +75,18 @@ class Microtower(Computer): pass +class ComputerMonitor(Device): + size = Float(description=m.ComputerMonitor.size.comment, validate=Range(2, 150)) + technology = EnumField(ComputerMonitorTechnologies, + description=m.ComputerMonitor.technology.comment) + resolution_width = Integer(data_key='resolutionWidth', + validate=Range(10, 20000), + description=m.ComputerMonitor.resolution_width.comment) + resolution_height = Integer(data_key='resolutionHeight', + validate=Range(10, 20000), + description=m.ComputerMonitor.resolution_height.comment) + + class Component(Device): parent = NestedOn(Device, dump_only=True) diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py index cc090c5d..ac07c8d6 100644 --- a/ereuse_devicehub/resources/enums.py +++ b/ereuse_devicehub/resources/enums.py @@ -149,3 +149,14 @@ class DataStorageInterface(Enum): ATA = 'ATA' USB = 'USB' PCI = 'PCI' + + +@unique +class ComputerMonitorTechnologies(Enum): + CRT = 'Cathode ray tube (CRT)' + TFT = 'Thin-film-transistor liquid-crystal (TFT)' + LED = 'LED-backlit (LED)' + PDP = 'Plasma display panel (Plasma)' + LCD = 'Liquid-crystal display (any of TFT, LED, Blue Phase, IPS)' + OLED = 'Organic light-emitting diode (OLED)' + AMOLED = 'Organic light-emitting diode (AMOLED)' diff --git a/ereuse_devicehub/resources/event/__init__.py b/ereuse_devicehub/resources/event/__init__.py index 6d323ae1..12e38732 100644 --- a/ereuse_devicehub/resources/event/__init__.py +++ b/ereuse_devicehub/resources/event/__init__.py @@ -1,7 +1,7 @@ from typing import Callable, Iterable, Tuple from ereuse_devicehub.resources.device.sync import Sync -from ereuse_devicehub.resources.event.schemas import Add, AggregateRate, Benchmark, \ +from ereuse_devicehub.resources.event.schemas import Add, AggregateRate, AppRate, Benchmark, \ BenchmarkDataStorage, BenchmarkProcessor, BenchmarkProcessorSysbench, BenchmarkRamSysbench, \ BenchmarkWithRate, EraseBasic, EraseSectors, Event, Install, PhotoboxSystemRate, \ PhotoboxUserRate, Rate, Remove, Snapshot, Step, StepRandom, StepZero, StressTest, Test, \ @@ -65,6 +65,10 @@ class PhotoboxSystemRateDef(RateDef): SCHEMA = PhotoboxSystemRate +class AppRateDef(RateDef): + SCHEMA = AppRate + + class InstallDef(EventDef): SCHEMA = Install diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index d26b6f60..5ba10cc0 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -2,14 +2,6 @@ from collections import Iterable from typing import Set, Union from uuid import uuid4 -from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Device -from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \ - FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \ - SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength -from ereuse_devicehub.resources.image.models import Image -from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing -from ereuse_devicehub.resources.user.models import User from flask import g from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \ Float, ForeignKey, Interval, JSON, SmallInteger, Unicode, event @@ -20,6 +12,14 @@ from sqlalchemy.orm import backref, relationship from sqlalchemy.orm.events import AttributeEvents as Events from sqlalchemy.util import OrderedSet +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Device +from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \ + FunctionalityRange, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \ + SnapshotExpectedEvents, SnapshotSoftware, TestHardDriveLength +from ereuse_devicehub.resources.image.models import Image +from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing +from ereuse_devicehub.resources.user.models import User from teal.db import ArrayOfEnum, CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \ POLYMORPHIC_ON, StrictVersionType, check_range @@ -256,15 +256,18 @@ class StepRandom(Step): class Snapshot(JoinedTableMixin, EventWithOneDevice): - uuid = Column(UUID(as_uuid=True), nullable=False, unique=True) + uuid = Column(UUID(as_uuid=True), unique=True) version = Column(StrictVersionType(STR_SM_SIZE), nullable=False) software = Column(DBEnum(SnapshotSoftware), nullable=False) - elapsed = Column(Interval, nullable=False) + elapsed = Column(Interval) + elapsed.comment = """ + For Snapshots made with Workbench, the total amount of time + it took to complete. + """ expected_events = Column(ArrayOfEnum(DBEnum(SnapshotExpectedEvents))) class Install(JoinedTableMixin, EventWithOneDevice): - name = Column(Unicode(STR_BIG_SIZE), nullable=False) elapsed = Column(Interval, nullable=False) @@ -290,6 +293,20 @@ class Rate(JoinedTableMixin, EventWithOneDevice): def rating_range(self) -> RatingRange: return RatingRange.from_score(self.rating) + @declared_attr + def __mapper_args__(cls): + """ + Defines inheritance. + + From `the guide `_ + """ + args = {POLYMORPHIC_ID: cls.t} + if cls.t == 'Rate': + args[POLYMORPHIC_ON] = cls.type + return args + class IndividualRate(Rate): pass @@ -319,18 +336,26 @@ class RateAggregateRate(db.Model): primary_key=True) -class WorkbenchRate(IndividualRate): +class ManualRate(IndividualRate): id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) + labelling = Column(Boolean) + appearance_range = Column(DBEnum(AppearanceRange)) + functionality_range = Column(DBEnum(FunctionalityRange)) + + +class WorkbenchRate(ManualRate): + id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id), primary_key=True) processor = Column(Float(decimal_return_scale=2), check_range('processor', *RATE_POSITIVE)) ram = Column(Float(decimal_return_scale=2), check_range('ram', *RATE_POSITIVE)) data_storage = Column(Float(decimal_return_scale=2), check_range('data_storage', *RATE_POSITIVE)) graphic_card = Column(Float(decimal_return_scale=2), check_range('graphic_card', *RATE_POSITIVE)) - labelling = Column(Boolean) bios = Column(DBEnum(Bios)) - appearance_range = Column(DBEnum(AppearanceRange)) - functionality_range = Column(DBEnum(FunctionalityRange)) + + +class AppRate(ManualRate): + pass class PhotoboxRate(IndividualRate): @@ -369,6 +394,20 @@ class PhotoboxSystemRate(PhotoboxRate): class Test(JoinedTableMixin, EventWithOneDevice): elapsed = Column(Interval, nullable=False) + @declared_attr + def __mapper_args__(cls): + """ + Defines inheritance. + + From `the guide `_ + """ + args = {POLYMORPHIC_ID: cls.t} + if cls.t == 'Test': + args[POLYMORPHIC_ON] = cls.type + return args + class TestDataStorage(Test): id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True) @@ -396,6 +435,20 @@ class StressTest(Test): class Benchmark(JoinedTableMixin, EventWithOneDevice): elapsed = Column(Interval) + @declared_attr + def __mapper_args__(cls): + """ + Defines inheritance. + + From `the guide `_ + """ + args = {POLYMORPHIC_ID: cls.t} + if cls.t == 'Benchmark': + args[POLYMORPHIC_ON] = cls.type + return args + class BenchmarkDataStorage(Benchmark): id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True) diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index 48923df9..f04c67d3 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -139,17 +139,26 @@ class AggregateRate(Rate): self.ratings = ... # type: Set[IndividualRate] -class WorkbenchRate(IndividualRate): +class ManualRate(IndividualRate): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.labelling = ... # type: bool + self.appearance_range = ... # type: AppearanceRange + self.functionality_range = ... # type: FunctionalityRange + + +class WorkbenchRate(ManualRate): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.processor = ... # type: float self.ram = ... # type: float self.data_storage = ... # type: float self.graphic_card = ... # type: float - self.labelling = ... # type: bool self.bios = ... # type: Bios - self.appearance_range = ... # type: AppearanceRange - self.functionality_range = ... # type: FunctionalityRange + + +class AppRate(ManualRate): + pass class PhotoboxRate(IndividualRate): diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 91c6d81e..4a478400 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -1,3 +1,10 @@ +from flask import current_app as app +from marshmallow import ValidationError, validates_schema +from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \ + UUID +from marshmallow.validate import Length, Range +from marshmallow_enum import EnumField + from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.device.schemas import Component, Device from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \ @@ -6,13 +13,6 @@ from ereuse_devicehub.resources.event import models as m from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.user.schemas import User -from flask import current_app as app -from marshmallow import ValidationError, validates_schema -from marshmallow.fields import Boolean, DateTime, Float, Integer, List, Nested, String, TimeDelta, \ - UUID -from marshmallow.validate import Length, Range -from marshmallow_enum import EnumField - from teal.marshmallow import Version from teal.resource import Schema @@ -124,7 +124,7 @@ class PhotoboxRate(IndividualRate): # todo Image -class PhotoboxUserRate(PhotoboxRate): +class PhotoboxUserRate(IndividualRate): assembling = Integer() parts = Integer() buttons = Integer() @@ -135,18 +135,11 @@ class PhotoboxUserRate(PhotoboxRate): dirt = Integer() -class PhotoboxSystemRate(PhotoboxRate): +class PhotoboxSystemRate(IndividualRate): pass -class WorkbenchRate(IndividualRate): - processor = Float() - ram = Float() - data_storage = Float() - graphic_card = Float() - labelling = Boolean(description='Sets if there are labels stuck that should be removed.') - bios = EnumField(Bios, description='How difficult it has been to set the bios to ' - 'boot from the network.') +class ManualRate(IndividualRate): appearance_range = EnumField(AppearanceRange, required=True, data_key='appearanceRange', @@ -156,6 +149,20 @@ class WorkbenchRate(IndividualRate): required=True, data_key='functionalityRange', description='Grades the defects of a device that affect its usage.') + labelling = Boolean(description='Sets if there are labels stuck that should be removed.') + + +class AppRate(ManualRate): + pass + + +class WorkbenchRate(ManualRate): + processor = Float() + ram = Float() + data_storage = Float() + graphic_card = Float() + bios = EnumField(Bios, description='How difficult it has been to set the bios to ' + 'boot from the network.') class Install(EventWithOneDevice): @@ -172,7 +179,7 @@ class Snapshot(EventWithOneDevice): See docs for more info. """ - uuid = UUID(required=True) + uuid = UUID() software = EnumField(SnapshotSoftware, required=True, description='The software that generated this Snapshot.') @@ -185,7 +192,7 @@ class Snapshot(EventWithOneDevice): 'the async Snapshot.') device = NestedOn(Device) - elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) + elapsed = TimeDelta(precision=TimeDelta.SECONDS) components = NestedOn(Component, many=True, description='A list of components that are inside of the device' @@ -206,10 +213,29 @@ class Snapshot(EventWithOneDevice): @validates_schema def validate_components_only_workbench(self, data: dict): if data['software'] != SnapshotSoftware.Workbench: - if data['components'] is not None: + if data.get('components', None) is not None: raise ValidationError('Only Workbench can add component info', field_names=['components']) + @validates_schema + def validate_only_workbench_fields(self, data: dict): + """Ensures workbench has ``elapsed`` and ``uuid`` and no others.""" + # todo test + if data['software'] == SnapshotSoftware.Workbench: + if not data.get('uuid', None): + raise ValidationError('Snapshots from Workbench must have uuid', + field_names=['uuid']) + if not data.get('elapsed', None): + raise ValidationError('Snapshots from Workbench must have elapsed', + field_names=['elapsed']) + else: + if data.get('uuid', None): + raise ValidationError('Only Snapshots from Workbench can have uuid', + field_names=['uuid']) + if data.get('elapsed', None): + raise ValidationError('Only Snapshots from Workbench can have elapsed', + field_names=['elapsed']) + class Test(EventWithOneDevice): elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py index 14067471..c38e4f9e 100644 --- a/ereuse_devicehub/resources/event/views.py +++ b/ereuse_devicehub/resources/event/views.py @@ -8,7 +8,8 @@ from sqlalchemy.util import OrderedSet from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.models import Component, Computer from ereuse_devicehub.resources.enums import RatingSoftware, SnapshotSoftware -from ereuse_devicehub.resources.event.models import Event, Snapshot, TestDataStorage, WorkbenchRate +from ereuse_devicehub.resources.event.models import Event, ManualRate, Snapshot, TestDataStorage, \ + WorkbenchRate from teal.resource import View @@ -40,14 +41,15 @@ class SnapshotView(View): # Remove new events from devices so they don't interfere with sync events_device = set(e for e in device.events_one) - events_components = tuple(set(e for e in component.events_one) for component in components) device.events_one.clear() - for component in components: - component.events_one.clear() + if components: + events_components = tuple(set(e for e in c.events_one) for c in components) + for component in components: + component.events_one.clear() # noinspection PyArgumentList assert not device.events_one - assert all(not c.events_one for c in components) + assert all(not c.events_one for c in components) if components else True db_device, remove_events = self.resource_def.sync.run(device, components) snapshot.device = db_device snapshot.events |= remove_events | events_device @@ -56,19 +58,21 @@ class SnapshotView(View): ordered_components = OrderedSet(x for x in snapshot.components) for event in events_device: + if isinstance(event, ManualRate): + event.algorithm_software = RatingSoftware.Ereuse + event.algorithm_version = StrictVersion('1.0') if isinstance(event, WorkbenchRate): # todo process workbench rate event.data_storage = 2 event.graphic_card = 4 event.processor = 1 - event.algorithm_software = RatingSoftware.Ereuse - event.algorithm_version = StrictVersion('1.0') # Add the new events to the db-existing devices and components db_device.events_one |= events_device - for component, events in zip(ordered_components, events_components): - component.events_one |= events - snapshot.events |= events + if components: + for component, events in zip(ordered_components, events_components): + component.events_one |= events + snapshot.events |= events db.session.add(snapshot) db.session.commit() diff --git a/ereuse_devicehub/resources/user/__init__.py b/ereuse_devicehub/resources/user/__init__.py index f3990e5f..82162776 100644 --- a/ereuse_devicehub/resources/user/__init__.py +++ b/ereuse_devicehub/resources/user/__init__.py @@ -1,7 +1,6 @@ from click import argument, option from flask import current_app as app -from ereuse_devicehub import devicehub from ereuse_devicehub.db import db from ereuse_devicehub.resources.user.models import Organization, User from ereuse_devicehub.resources.user.schemas import User as UserS @@ -16,7 +15,7 @@ class UserDef(Resource): ID_CONVERTER = Converters.uuid AUTH = True - def __init__(self, app: 'devicehub.Devicehub', import_name=__package__, static_folder=None, + def __init__(self, app, import_name=__package__, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None): cli_commands = ((self.create_user, 'create-user'),) diff --git a/setup.py b/setup.py index 76cc7ae8..4db038b0 100644 --- a/setup.py +++ b/setup.py @@ -11,14 +11,16 @@ setup( include_package_data=True, description='A system to manage devices focusing reuse.', install_requires=[ - 'teal>=0.2.0a1', + 'teal>=0.2.0a2', 'marshmallow_enum', - 'ereuse-utils [Naming]>=0.3.0b1', + 'ereuse-utils [Naming]>=0.3.0b2', 'psycopg2-binary', 'sqlalchemy-utils', 'requests', 'requests-toolbelt', - 'hashids' + 'hashids', + 'tqdm', + 'click-spinner' ], tests_requires=[ 'pytest', diff --git a/tests/conftest.py b/tests/conftest.py index 09a33485..3aec3c61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,11 +53,12 @@ def app_context(app: Devicehub): def user(app: Devicehub) -> UserClient: """Gets a client with a logged-in dummy user.""" with app.app_context(): - user = create_user() + password = 'foo' + user = create_user(password=password) client = UserClient(application=app, response_wrapper=app.response_class, email=user.email, - password='foo') + password=password) client.user, _ = client.login(client.email, client.password) return client diff --git a/tests/files/computer-monitor.snapshot.yaml b/tests/files/computer-monitor.snapshot.yaml new file mode 100644 index 00000000..aefc5817 --- /dev/null +++ b/tests/files/computer-monitor.snapshot.yaml @@ -0,0 +1,17 @@ +type: Snapshot +software: AndroidApp +version: '1.0' +device: + type: ComputerMonitor + technology: LCD + manufacturer: Dell + model: 1707FPF + serialNumber: CN0FP446728728541C8S + resolutionWidth: 1920 + resolutionHeight: 1080 + size: 21.5 + events: + - type: AppRate + appearanceRange: A + functionalityRange: C + labelling: False diff --git a/tests/test_device.py b/tests/test_device.py index 4a5f11b3..67a6e7f7 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -10,11 +10,12 @@ from ereuse_devicehub.client import UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.device.exceptions import NeedsId -from ereuse_devicehub.resources.device.models import Component, Computer, Desktop, Device, \ - GraphicCard, Laptop, Microtower, Motherboard, NetworkAdapter +from ereuse_devicehub.resources.device.models import Component, Computer, ComputerMonitor, Desktop, \ + Device, GraphicCard, Laptop, Microtower, Motherboard, NetworkAdapter from ereuse_devicehub.resources.device.schemas import Device as DeviceS from ereuse_devicehub.resources.device.sync import MismatchBetweenTags, MismatchBetweenTagsAndHid, \ Sync +from ereuse_devicehub.resources.enums import ComputerMonitorTechnologies from ereuse_devicehub.resources.event.models import Remove, Test from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.user import User @@ -396,3 +397,16 @@ def test_get_devices(app: Devicehub, user: UserClient): assert tuple(d['id'] for d in devices) == (1, 2, 3, 4, 5) assert tuple(d['type'] for d in devices) == ('Desktop', 'Microtower', 'Laptop', 'NetworkAdapter', 'GraphicCard') + + +@pytest.mark.usefixtures('app_context') +def test_computer_monitor(): + m = ComputerMonitor(technology=ComputerMonitorTechnologies.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() diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index f41b73ad..14fe80e0 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -69,11 +69,13 @@ def snapshot_and_check(user: UserClient, assert event['type'] != 'Receive', 'All Remove events must be before the Add ones' assert input_snapshot['device'] assert_similar_device(input_snapshot['device'], snapshot['device']) - assert_similar_components(input_snapshot['components'], snapshot['components']) + if input_snapshot.get('components', None): + assert_similar_components(input_snapshot['components'], snapshot['components']) assert all(c['parent'] == snapshot['device']['id'] for c in snapshot['components']), \ 'Components must be in their parent' if perform_second_snapshot: - input_snapshot['uuid'] = uuid4() + if 'uuid' in input_snapshot: + input_snapshot['uuid'] = uuid4() return snapshot_and_check(user, input_snapshot, event_types, perform_second_snapshot=False) else: return snapshot @@ -330,3 +332,24 @@ def test_erase(user: UserClient): assert step['secureRandomSteps'] == 1 assert step['cleanWithZeros'] is True assert 'num' not in step + + +def test_snapshot_computer_monitor(user: UserClient): + s = file('computer-monitor.snapshot') + snapshot_and_check(user, s, event_types=('AppRate',)) + + +def test_snapshot_components_none(): + """ + Tests that a snapshot without components does not + remove them from the computer. + """ + # todo test + pass + + +def test_snapshot_components_empty(): + """ + Tests that a snapshot whose components are an empty list remove + all its components. + """