diff --git a/docs/states.puml b/docs/states.puml index 3582b829..adb51475 100644 --- a/docs/states.puml +++ b/docs/states.puml @@ -12,6 +12,7 @@ state Attributes { state Usufructuarees state Reservees state "Physical\nPossessor" + state "Waste\n\Product" } state Physical { @@ -35,8 +36,17 @@ state Trading { Reserved --> Cancelled : Cancel Sold --> Cancelled : Cancel Sold --> Payed : Pay - Registered --> ToBeDisposed - ToBeDisposed --> Disposed : DisposeProduct + Registered --> ToBeDisposed : ToDisposeProduct + ToBeDisposed --> ProductDisposed : DisposeProduct + Registered --> Donated: Donate + Registered --> Renting: Rent + Donated --> Cancelled : Cancel + Renting --> Cancelled : Cancel +} + +state DataStoragePrivacyCompliance { + state Erased + state Destroyed } diff --git a/ereuse_devicehub/dummy/files/computer-monitor.snapshot.yaml b/ereuse_devicehub/dummy/files/computer-monitor.snapshot.yaml index c4988cf8..66f0bc08 100644 --- a/ereuse_devicehub/dummy/files/computer-monitor.snapshot.yaml +++ b/ereuse_devicehub/dummy/files/computer-monitor.snapshot.yaml @@ -11,7 +11,7 @@ device: resolutionHeight: 1080 size: 21.5 events: - - type: AppRate + - type: ManualRate appearanceRange: A functionalityRange: C labelling: False diff --git a/ereuse_devicehub/dummy/files/smartphone.snapshot.yaml b/ereuse_devicehub/dummy/files/smartphone.snapshot.yaml index 10d378f1..3b82dfe4 100644 --- a/ereuse_devicehub/dummy/files/smartphone.snapshot.yaml +++ b/ereuse_devicehub/dummy/files/smartphone.snapshot.yaml @@ -8,7 +8,7 @@ device: serialNumber: ABCDEF imei: 35686800-004141-20 events: - - type: AppRate + - type: ManualRate appearanceRange: A functionalityRange: B labelling: False diff --git a/ereuse_devicehub/dummy/files/workbench-server-1.snapshot.yaml b/ereuse_devicehub/dummy/files/workbench-server-1.snapshot.yaml index d4a24239..e533d06e 100644 --- a/ereuse_devicehub/dummy/files/workbench-server-1.snapshot.yaml +++ b/ereuse_devicehub/dummy/files/workbench-server-1.snapshot.yaml @@ -26,6 +26,7 @@ device: functionalityRange: B - type: BenchmarkRamSysbench rate: 2444 + elapsed: 1 components: - type: GraphicCard serialNumber: gc1-1s @@ -35,22 +36,27 @@ components: serialNumber: rm1-1s model: rm1-1ml manufacturer: rm1-1mr + size: 1024 - type: RamModule serialNumber: rm2-1s model: rm2-1ml manufacturer: rm2-1mr + size: 1024 - type: Processor - model: p1-1s + model: p1-1ml manufacturer: p1-1mr events: - type: BenchmarkProcessor rate: 2410 + elapsed: 44 - type: BenchmarkProcessorSysbench rate: 4400 + elapsed: 44 - type: SolidStateDrive serialNumber: ssd1-1s model: ssd1-1ml manufacturer: ssd1-1mr + size: 1100 events: - type: BenchmarkDataStorage readSpeed: 20 @@ -78,7 +84,24 @@ components: - type: BenchmarkDataStorage readSpeed: 10 writeSpeed: 5 + elapsed: 20 - type: Motherboard serialNumber: mb1-1s model: mb1-1ml manufacturer: mb1-1mr +- type: NetworkAdapter + serialNumber: na1-s + model: na1-1ml + manufacturer: na1-1mr + speed: 1000 + wireless: False +- type: NetworkAdapter + serialNumber: na2-s + model: na2-1ml + manufacturer: na2-1mr + wireless: True + speed: 58 +- type: RamModule + serialNumber: rm3-1s + model: rm3-1ml + manufacturer: rm3-1mr diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index e66c6888..e5d6179f 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -21,8 +21,8 @@ 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, DisplayTech, \ - RamFormat, RamInterface +from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \ + DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing @@ -44,19 +44,22 @@ class Device(Thing): model = Column(Unicode(), check_lower('model')) manufacturer = Column(Unicode(), check_lower('manufacturer')) serial_number = Column(Unicode(), check_lower('serial_number')) - weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 3)) + weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5)) weight.comment = """ The weight of the device in Kgm. """ - width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 3)) + width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5)) width.comment = """ The width of the device in meters. """ - height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 3)) + height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5)) height.comment = """ The height of the device in meters. """ - depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3)) + depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5)) + depth.comment = """ + The depth of the device in meters. + """ color = Column(ColorType) color.comment = """The predominant color of the device.""" @@ -98,6 +101,54 @@ class Device(Thing): """The URL where to GET this device.""" return urlutils.URL(url_for_resource(Device, item_id=self.id)) + @property + def rate(self): + """Gets the last aggregate rate.""" + with suppress(LookupError, ValueError): + from ereuse_devicehub.resources.event.models import AggregateRate + return self.last_event_of(AggregateRate) + + @property + def price(self): + """Gets the actual Price of the device or None + if no price has ever been set.""" + with suppress(LookupError, ValueError): + from ereuse_devicehub.resources.event.models import Price + return self.last_event_of(Price) + + @property + def trading(self): + """The actual trading state or None if there is no trading info.""" + from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): + event = self.last_event_of(*states.Trading.events()) + return states.Trading(event.__class__) + + @property + def physical(self): + """The actual physical state, None if there is no state.""" + from ereuse_devicehub.resources.device import states + with suppress(LookupError, ValueError): + event = self.last_event_of(*states.Physical.events()) + return states.Physical(event.__class__) + + @property + def physical_possessor(self): + """The actual physical possessor or None. + + The physical possessor is the Agent that has physically + the device. It differs from legal owners, usufructuarees + or reserves in that the physical possessor does not have + a legal relation per se with the device, but it is the one + that has it physically. As an example, a transporter could + be a physical possessor of a device although it does not + own it legally. + """ + from ereuse_devicehub.resources.event.models import Receive + with suppress(LookupError): + event = self.last_event_of(Receive) + return event.agent + @declared_attr def __mapper_args__(cls): """ @@ -112,6 +163,16 @@ class Device(Thing): args[POLYMORPHIC_ON] = cls.type return args + def last_event_of(self, *types): + """Gets the last event of the given types. + + :raise LookupError: Device has not an event of the given type. + """ + try: + return next(e for e in reversed(self.events) if isinstance(e, types)) + except StopIteration: + raise LookupError('{!r} does not contain events of types {}.'.format(self, types)) + def __lt__(self, other): return self.id < other.id @@ -169,31 +230,38 @@ class Computer(Device): @property def ram_size(self) -> int: """The total of RAM memory the computer has.""" - return sum(ram.size for ram in self.components if isinstance(ram, RamModule)) + return sum(ram.size or 0 for ram in self.components if isinstance(ram, RamModule)) @property def data_storage_size(self) -> int: """The total of data storage the computer has.""" - return sum(ds.size for ds in self.components if isinstance(ds, DataStorage)) + return sum(ds.size or 0 for ds in self.components if isinstance(ds, DataStorage)) @property def processor_model(self) -> str: """The model of one of the processors of the computer.""" - return next(p.model for p in self.components if isinstance(p, Processor)) + return next((p.model for p in self.components if isinstance(p, Processor)), None) @property def graphic_card_model(self) -> str: """The model of one of the graphic cards of the computer.""" - return next(p.model for p in self.components if isinstance(p, GraphicCard)) + return next((p.model for p in self.components if isinstance(p, GraphicCard)), None) @property def network_speeds(self) -> List[int]: - """Returns two speeds: the first for the eth and the - second for the wifi networks, or 0 respectively if not found. + """Returns two values representing the speeds of the network + adapters of the device. + + 1. The max Ethernet speed of the computer, 0 if ethernet + adaptor exists but its speed is unknown, None if no eth + adaptor exists. + 2. The max WiFi speed of the computer, 0 if computer has + WiFi but its speed is unknown, None if no WiFi adaptor + exists. """ - speeds = [0, 0] + speeds = [None, None] for net in (c for c in self.components if isinstance(c, NetworkAdapter)): - speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless]) + speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless] or 0) return speeds def __format__(self, format_spec): @@ -322,6 +390,15 @@ class DataStorage(JoinedComponentTableMixin, Component): """ interface = Column(DBEnum(DataStorageInterface)) + @property + def privacy(self): + """Returns the privacy compliance state of the data storage.""" + # todo add physical destruction event + from ereuse_devicehub.resources.event.models import EraseBasic + with suppress(LookupError): + erase = self.last_event_of(EraseBasic) + return DataStoragePrivacyCompliance.from_erase(erase) + def __format__(self, format_spec): v = super().__format__(format_spec) if 's' in format_spec: @@ -353,7 +430,7 @@ class NetworkMixin: speed.comment = """ The maximum speed this network adapter can handle, in mbps. """ - wireless = Column(Boolean) + wireless = Column(Boolean, nullable=False, default=False) wireless.comment = """ Whether it is a wireless interface. """ diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi index b2138d78..d53de794 100644 --- a/ereuse_devicehub/resources/device/models.pyi +++ b/ereuse_devicehub/resources/device/models.pyi @@ -1,4 +1,4 @@ -from typing import Dict, List, Set +from typing import Dict, List, Set, Type, Union from boltons import urlutils from boltons.urlutils import URL @@ -7,10 +7,11 @@ from sqlalchemy import Column, Integer from sqlalchemy.orm import relationship from teal.db import Model -from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ - RamFormat, RamInterface -from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \ - EventWithOneDevice +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 +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 from ereuse_devicehub.resources.models import Thing @@ -44,10 +45,10 @@ class Device(Thing): self.height = ... # type: float self.depth = ... # type: float self.color = ... # type: Color - self.events = ... # type: List[Event] + self.events = ... # type: List[e.Event] self.physical_properties = ... # type: Dict[str, object or None] - self.events_multiple = ... # type: Set[EventWithMultipleDevices] - self.events_one = ... # type: Set[EventWithOneDevice] + self.events_multiple = ... # type: Set[e.EventWithMultipleDevices] + self.events_one = ... # type: Set[e.EventWithOneDevice] self.images = ... # type: ImageList self.tags = ... # type: Set[Tag] self.lots = ... # type: Set[Lot] @@ -56,6 +57,30 @@ class Device(Thing): def url(self) -> urlutils.URL: pass + @property + def rate(self) -> Union[e.AggregateRate, None]: + pass + + @property + def price(self) -> Union[e.Price, None]: + pass + + @property + def trading(self) -> Union[states.Trading, None]: + pass + + @property + def physical(self) -> Union[states.Physical, None]: + pass + + @property + def physical_possessor(self) -> Union[Agent, None]: + pass + + def last_event_of(self, *types: Type[e.Event]) -> e.Event: + pass + + class DisplayMixin: technology = ... # type: Column size = ... # type: Column @@ -77,7 +102,7 @@ class Computer(DisplayMixin, Device): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.components = ... # type: Set[Component] - self.events_parent = ... # type: Set[Event] + self.events_parent = ... # type: Set[e.Event] self.chassis = ... # type: ComputerChassis @property @@ -104,6 +129,7 @@ class Computer(DisplayMixin, Device): def network_speeds(self) -> List[int]: pass + class Desktop(Computer): pass @@ -155,7 +181,7 @@ class Component(Device): super().__init__(**kwargs) self.parent_id = ... # type: int self.parent = ... # type: Computer - self.events_components = ... # type: Set[Event] + self.events_components = ... # type: Set[e.Event] def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component': pass @@ -178,6 +204,10 @@ class DataStorage(Component): self.size = ... # type: int self.interface = ... # type: DataStorageInterface + @property + def privacy(self) -> DataStoragePrivacyCompliance: + pass + class HardDrive(DataStorage): pass diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 1e4c303e..857ecbd9 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -1,5 +1,5 @@ from marshmallow import post_load, pre_load -from marshmallow.fields import Boolean, Float, Integer, Str, String +from marshmallow.fields import Boolean, Float, Integer, List, Str, String from marshmallow.validate import Length, OneOf, Range from sqlalchemy.util import OrderedSet from stdnum import imei, meid @@ -7,9 +7,9 @@ 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 -from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ - RamFormat, RamInterface +from ereuse_devicehub.resources.device import models as m, states +from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \ + DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing, UnitCodes @@ -24,13 +24,19 @@ class Device(Thing): model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE)) manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE)) serial_number = SanitizedStr(lower=True, data_key='serialNumber') - weight = Float(validate=Range(0.1, 3), unit=UnitCodes.kgm, description=m.Device.weight.comment) - width = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.width.comment) - height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.height.comment) + weight = Float(validate=Range(0.1, 5), unit=UnitCodes.kgm, description=m.Device.weight.comment) + width = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.width.comment) + height = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.height.comment) + depth = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.depth.comment) events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__) events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet) url = URL(dump_only=True, description=m.Device.url.__doc__) lots = NestedOn('Lot', many=True, dump_only=True) + rate = NestedOn('AggregateRate', dump_only=True) + price = NestedOn('Price', dump_only=True) + trading = EnumField(states.Trading, dump_only=True) + physical = EnumField(states.Physical, dump_only=True) + physical_possessor = NestedOn('Agent', dump_only=True, data_key='physicalPossessor') @pre_load def from_events_to_events_one(self, data: dict): @@ -60,6 +66,11 @@ class Device(Thing): class Computer(Device): components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet) chassis = EnumField(ComputerChassis, required=True) + ram_size = Integer(dump_only=True, data_key='ramSize') + data_storage_size = Integer(dump_only=True, data_key='dataStorageSize') + processor_model = Str(dump_only=True, data_key='processorModel') + graphic_card_model = Str(dump_only=True, data_key='graphicCardModel') + network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds') class Desktop(Computer): @@ -148,6 +159,7 @@ class DataStorage(Component): unit=UnitCodes.mbyte, description=m.DataStorage.size.comment) interface = EnumField(DataStorageInterface) + privacy = EnumField(DataStoragePrivacyCompliance, dump_only=True) class HardDrive(DataStorage): diff --git a/ereuse_devicehub/resources/device/states.py b/ereuse_devicehub/resources/device/states.py new file mode 100644 index 00000000..d10ce52f --- /dev/null +++ b/ereuse_devicehub/resources/device/states.py @@ -0,0 +1,30 @@ +from enum import Enum + +from ereuse_devicehub.resources.event import models as e + + +class State(Enum): + @classmethod + def events(cls): + """Events participating in this state.""" + return (s.value for s in cls) + + +class Trading(State): + Reserved = e.Reserve + Cancelled = e.CancelTrade + Sold = e.Sell + Donated = e.Donate + Renting = e.Rent + # todo add Pay = e.Pay + ToBeDisposed = e.ToDisposeProduct + ProductDisposed = e.DisposeProduct + + +class Physical(State): + ToBeRepaired = e.ToRepair + Repaired = e.Repair + Preparing = e.ToPrepare + Prepared = e.Prepare + ReadyToBeUsed = e.ReadyToUse + InUse = e.Live diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py index 1303e0a0..41b561d8 100644 --- a/ereuse_devicehub/resources/enums.py +++ b/ereuse_devicehub/resources/enums.py @@ -235,3 +235,21 @@ class ReceiverRole(Enum): CollectionPoint = 'A collection point.' RecyclingPoint = 'A recycling point.' Transporter = 'An user that ships the devices to another one.' + + +class DataStoragePrivacyCompliance(Enum): + EraseBasic = 'EraseBasic' + EraseBasicError = 'EraseBasicError' + EraseSectors = 'EraseSectors' + EraseSectorsError = 'EraseSectorsError' + Destruction = 'Destruction' + DestructionError = 'DestructionError' + + @classmethod + def from_erase(cls, erasure) -> 'DataStoragePrivacyCompliance': + """Returns the correct enum depending of the passed-in erasure.""" + from ereuse_devicehub.resources.event.models import EraseSectors + if isinstance(erasure, EraseSectors): + return cls.EraseSectors if not erasure.error else cls.EraseSectorsError + else: + return cls.EraseBasic if not erasure.error else cls.EraseBasicError diff --git a/ereuse_devicehub/resources/event/__init__.py b/ereuse_devicehub/resources/event/__init__.py index 561bf961..efc7fd45 100644 --- a/ereuse_devicehub/resources/event/__init__.py +++ b/ereuse_devicehub/resources/event/__init__.py @@ -74,9 +74,9 @@ class PhotoboxSystemRateDef(RateDef): SCHEMA = schemas.PhotoboxSystemRate -class AppRateDef(RateDef): +class ManualRateDef(RateDef): VIEW = None - SCHEMA = schemas.AppRate + SCHEMA = schemas.ManualRate class PriceDef(EventDef): @@ -98,7 +98,8 @@ class SnapshotDef(EventDef): VIEW = SnapshotView SCHEMA = schemas.Snapshot - def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None, static_url_path=None, + def __init__(self, app, import_name=__name__.split('.')[0], 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, diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 650426d2..3bbc44d0 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -1,5 +1,6 @@ from collections import Iterable from datetime import datetime, timedelta +from distutils.version import StrictVersion from typing import Set, Union from uuid import uuid4 @@ -24,17 +25,12 @@ from ereuse_devicehub.db import db from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \ Device, Laptop, Server -from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \ +from ereuse_devicehub.resources.enums import AppearanceRange, Bios, \ FunctionalityRange, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \ ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength -from ereuse_devicehub.resources.image.models import Image from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing from ereuse_devicehub.resources.user.models import User -""" -A quantity of money with a currency. -""" - class JoinedTableMixin: # noinspection PyMethodParameters @@ -54,7 +50,7 @@ class Event(Thing): incidence.comment = """ Should this event be reviewed due some anomaly? """ - closed = Column(Boolean, default=False, nullable=False) + closed = Column(Boolean, default=True, nullable=False) closed.comment = """ Whether the author has finished the event. After this is set to True, no modifications are allowed. @@ -389,30 +385,6 @@ class IndividualRate(Rate): pass -class AggregateRate(Rate): - id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) - ratings = relationship(IndividualRate, - backref=backref('aggregated_ratings', - lazy=True, - order_by=lambda: IndividualRate.created, - collection_class=OrderedSet), - secondary=lambda: RateAggregateRate.__table__, - order_by=lambda: IndividualRate.created, - collection_class=OrderedSet) - """The ratings this aggregateRate aggregates.""" - - -class RateAggregateRate(db.Model): - """ - Represents the ``many to many`` relationship between - ``Rate`` and ``AggregateRate``. - """ - rate_id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) - aggregate_rate_id = Column(UUID(as_uuid=True), - ForeignKey(AggregateRate.id), - primary_key=True) - - class ManualRate(IndividualRate): id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) labelling = Column(Boolean) @@ -432,51 +404,67 @@ class WorkbenchRate(ManualRate): # todo ensure for WorkbenchRate version and software are not None when inserting them - def ratings(self) -> Set['WorkbenchRate']: + def ratings(self): """ Computes all the possible rates taking this rating as a model. Returns a set of ratings, including this one, which is mutated. """ - from ereuse_rate.main import main + from ereuse_devicehub.resources.event.rate.main import main return main(self, **app.config.get_namespace('WORKBENCH_RATE_')) -class AppRate(ManualRate): - pass - - -class PhotoboxRate(IndividualRate): +class AggregateRate(Rate): id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) - image_id = Column(UUID(as_uuid=True), ForeignKey(Image.id), nullable=False) - image = relationship(Image, - uselist=False, - cascade=CASCADE_OWN, - single_parent=True, - primaryjoin=Image.id == image_id) + manual_id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id)) + manual = relationship(ManualRate, + backref=backref('aggregate_rate_manual', + lazy=True, + order_by=lambda: AggregateRate.created, + collection_class=OrderedSet), + primaryjoin=manual_id == ManualRate.id) + workbench_id = Column(UUID(as_uuid=True), ForeignKey(WorkbenchRate.id)) + workbench = relationship(WorkbenchRate, + backref=backref('aggregate_rate_workbench', + lazy=True, + order_by=lambda: AggregateRate.created, + collection_class=OrderedSet), + primaryjoin=workbench_id == WorkbenchRate.id) - # todo how to ensure phtoboxrate.device == image.image_list.device? + def __init__(self, *args, **kwargs) -> None: + kwargs.setdefault('version', StrictVersion('1.0')) + super().__init__(*args, **kwargs) + @property + def processor(self): + return self.workbench.processor -class PhotoboxUserRate(PhotoboxRate): - id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True) - assembling = Column(SmallInteger, check_range('assembling', *BOX_RATE_5), nullable=False) - parts = Column(SmallInteger, check_range('parts', *BOX_RATE_5), nullable=False) - buttons = Column(SmallInteger, check_range('buttons', *BOX_RATE_5), nullable=False) - dents = Column(SmallInteger, check_range('dents', *BOX_RATE_5), nullable=False) - decolorization = Column(SmallInteger, - check_range('decolorization', *BOX_RATE_5), - nullable=False) - scratches = Column(SmallInteger, check_range('scratches', *BOX_RATE_5), nullable=False) - tag_alignment = Column(SmallInteger, - check_range('tag_alignment', *BOX_RATE_3), - nullable=False) - tag_adhesive = Column(SmallInteger, check_range('tag_adhesive', *BOX_RATE_3), nullable=False) - dirt = Column(SmallInteger, check_range('dirt', *BOX_RATE_3), nullable=False) + @property + def ram(self): + return self.workbench.ram + @property + def data_storage(self): + return self.workbench.data_storage -class PhotoboxSystemRate(PhotoboxRate): - id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True) + @property + def graphic_card(self): + return self.workbench.graphic_card + + @property + def bios(self): + return self.workbench.bios + + @classmethod + def from_workbench_rate(cls, rate: WorkbenchRate): + aggregate = cls() + aggregate.rating = rate.rating + aggregate.software = rate.software + aggregate.appearance = rate.appearance + aggregate.functionality = rate.functionality + aggregate.device = rate.device + aggregate.workbench = rate + return aggregate class Price(JoinedWithOneDeviceMixin, EventWithOneDevice): diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index 8730fb31..8c68cf8e 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -19,7 +19,6 @@ from ereuse_devicehub.resources.device.models import Component, Computer, Device from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \ PriceSoftware, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, \ TestDataStorageLength -from ereuse_devicehub.resources.image.models import Image from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User @@ -78,7 +77,7 @@ class EventWithOneDevice(Event): class EventWithMultipleDevices(Event): - devices = ... # type: relationship + devices = ... # type: relationship def __init__(self, id=None, name=None, incidence=None, closed=None, error=None, description=None, start_time=None, end_time=None, snapshot=None, agent=None, @@ -165,11 +164,43 @@ class IndividualRate(Rate): class AggregateRate(Rate): + manual_id = ... # type: Column + manual = ... # type: relationship + workbench = ... # type: relationship + workbench_id = ... # type: Column + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - self.ratings = ... # type: Set[IndividualRate] + self.manual_id = ... # type: UUID + self.manual = ... # type: ManualRate + self.workbench = ... # type: WorkbenchRate + self.workbench_id = ... # type: UUID self.price = ... # type: Price + @property + def processor(self): + return self.workbench.processor + + @property + def ram(self): + return self.workbench.ram + + @property + def data_storage(self): + return self.workbench.data_storage + + @property + def graphic_card(self): + return self.workbench.graphic_card + + @property + def bios(self): + return self.workbench.bios + + @classmethod + def from_workbench_rate(cls, rate: WorkbenchRate) -> AggregateRate: + pass + class ManualRate(IndividualRate): def __init__(self, **kwargs) -> None: @@ -177,6 +208,7 @@ class ManualRate(IndividualRate): self.labelling = ... # type: bool self.appearance_range = ... # type: AppearanceRange self.functionality_range = ... # type: FunctionalityRange + self.aggregate_rate_manual = ... #type: AggregateRate class WorkbenchRate(ManualRate): @@ -187,34 +219,10 @@ class WorkbenchRate(ManualRate): self.data_storage = ... # type: float self.graphic_card = ... # type: float self.bios = ... # type: Bios + self.aggregate_rate_workbench = ... #type: AggregateRate - -class AppRate(ManualRate): - pass - - -class PhotoboxRate(IndividualRate): - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - self.num = ... # type: int - self.image = ... # type: Image - - -class PhotoboxUserRate(PhotoboxRate): - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - self.assembling = ... # type: int - self.parts = ... # type: int - self.buttons = ... # type: int - self.dents = ... # type: int - self.decolorization = ... # type: int - self.scratches = ... # type: int - self.tag_adhesive = ... # type: int - self.dirt = ... # type: int - - -class PhotoboxSystemRate(PhotoboxRate): - pass + def ratings(self) -> Set[Rate]: + pass class Price(EventWithOneDevice): @@ -237,8 +245,24 @@ class Price(EventWithOneDevice): class EreusePrice(Price): MULTIPLIER = ... # type: Dict + class Type: + def __init__(self) -> None: + super().__init__() + self.amount = ... # type: float + self.percentage = ... # type: float + + class Service: + def __init__(self) -> None: + super().__init__() + self.standard = ... # type: EreusePrice.Type + self.warranty2 = ... # type: EreusePrice.Type + def __init__(self, rating: AggregateRate, **kwargs) -> None: super().__init__(**kwargs) + self.retailer = ... # type: EreusePrice.Service + self.platform = ... # type: EreusePrice.Service + self.refurbisher = ... # type: EreusePrice.Service + self.warranty2 = ... # type: float class Test(EventWithOneDevice): diff --git a/ereuse_devicehub/resources/event/rate/__init__.py b/ereuse_devicehub/resources/event/rate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/resources/event/rate/main.py b/ereuse_devicehub/resources/event/rate/main.py new file mode 100644 index 00000000..69213ba7 --- /dev/null +++ b/ereuse_devicehub/resources/event/rate/main.py @@ -0,0 +1,78 @@ +from contextlib import suppress +from distutils.version import StrictVersion +from typing import Set, Union + +from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.enums import RatingSoftware +from ereuse_devicehub.resources.event.models import AggregateRate, EreusePrice, Rate, \ + WorkbenchRate +from ereuse_devicehub.resources.event.rate.workbench import v1_0 + +RATE_TYPES = { + WorkbenchRate: { + RatingSoftware.ECost: { + '1.0': v1_0.Rate() + }, + RatingSoftware.EMarket: { + } + } +} + + +def rate(device: Device, rate: Rate): + """ + Rates the passed-in ``rate`` using values from the rate itself + and the ``device``. + + This method mutates ``rate``. + + :param device: The device to use as a model. + :param rate: A half-filled rate. + """ + cls = rate.__class__ + assert cls in RATE_TYPES, 'Rate type {} not supported.'.format(cls) + assert rate.software in RATE_TYPES[cls], 'Rate soft {} not supported.'.format(rate.software) + assert str(rate.version) in RATE_TYPES[cls][rate.software], \ + 'Rate version {} not supported.'.format(rate.version) + RATE_TYPES[cls][rate.software][str(rate.version)].compute(device, rate) + + +def main(rating_model: WorkbenchRate, + software: RatingSoftware, + version: StrictVersion) -> Set[Union[WorkbenchRate, AggregateRate, EreusePrice]]: + """ + Generates all the rates (per software and version) for a given + half-filled rate acting as a model, and finally it generates + an ``AggregateRating`` with the rate that matches the + ``software`` and ``version``. + + This method mutates ``rating_model`` by fulfilling it and + ``rating_model.device`` by adding the new rates. + + :return: A set of rates with the ``rate`` value computed, where + the first rate is the ``rating_model``. + """ + assert rating_model.device + events = set() + for soft, value in RATE_TYPES[rating_model.__class__].items(): + for vers, func in value.items(): + if not rating_model.rating: # Fill the rating before creating another rate + rating = rating_model + else: # original rating was filled already; use a new one + rating = WorkbenchRate( + labelling=rating_model.labelling, + appearance_range=rating_model.appearance_range, + functionality_range=rating_model.functionality_range, + device=rating_model.device, + ) + rating.software = soft + rating.version = vers + rate(rating_model.device, rating) + events.add(rating) + if soft == software and vers == version: + aggregation = AggregateRate.from_workbench_rate(rating) + events.add(aggregation) + with suppress(ValueError): + # We will have exception if range == VERY_LOW + events.add(EreusePrice(aggregation)) + return events diff --git a/ereuse_devicehub/resources/event/rate/rate.py b/ereuse_devicehub/resources/event/rate/rate.py new file mode 100644 index 00000000..d6f4d0d0 --- /dev/null +++ b/ereuse_devicehub/resources/event/rate/rate.py @@ -0,0 +1,54 @@ +import math +from typing import Iterable + +from ereuse_devicehub.resources.device.models import Device +from ereuse_devicehub.resources.event.models import WorkbenchRate + + +class BaseRate: + """growing exponential from this value""" + CEXP = 0 + """growing lineal starting on this value""" + CLIN = 242 + """growing logarithmic starting on this value""" + CLOG = 0.5 + + """Processor has 50% of weight over total score, used in harmonic mean""" + PROCESSOR_WEIGHT = 0.5 + """Storage has 20% of weight over total score, used in harmonic mean""" + DATA_STORAGE_WEIGHT = 0.2 + """Ram has 30% of weight over total score, used in harmonic mean""" + RAM_WEIGHT = 0.3 + + def compute(self, device: Device, rate: WorkbenchRate): + raise NotImplementedError() + + @staticmethod + def norm(x, x_min, x_max): + return (x - x_min) / (x_max - x_min) + + @staticmethod + def rate_log(x): + return math.log10(2 * x) + 3.57 # todo magic number! + + @staticmethod + def rate_lin(x): + return 7 * x + 0.06 # todo magic number! + + @staticmethod + def rate_exp(x): + return math.exp(x) / (2 - math.exp(x)) + + @staticmethod + def harmonic_mean(weights: Iterable[float], rates: Iterable[float]): + return sum(weights) / sum(char / rate for char, rate in zip(weights, rates)) + + def harmonic_mean_rates(self, rate_processor, rate_storage, rate_ram): + """ + Merging components + """ + total_weights = self.PROCESSOR_WEIGHT + self.DATA_STORAGE_WEIGHT + self.RAM_WEIGHT + total_rate = self.PROCESSOR_WEIGHT / rate_processor \ + + self.DATA_STORAGE_WEIGHT / rate_storage \ + + self.RAM_WEIGHT / rate_ram + return total_weights / total_rate diff --git a/ereuse_devicehub/resources/event/rate/workbench/__init__.py b/ereuse_devicehub/resources/event/rate/workbench/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ereuse_devicehub/resources/event/rate/workbench/v1_0.py b/ereuse_devicehub/resources/event/rate/workbench/v1_0.py new file mode 100644 index 00000000..0d3a385b --- /dev/null +++ b/ereuse_devicehub/resources/event/rate/workbench/v1_0.py @@ -0,0 +1,253 @@ +from enum import Enum +from itertools import groupby +from typing import Iterable + +from ereuse_devicehub.resources.device.models import Computer, DataStorage, Desktop, Laptop, \ + Processor, RamModule, Server +from ereuse_devicehub.resources.event.models import BenchmarkDataStorage, BenchmarkProcessor, \ + WorkbenchRate +# todo if no return assign then rate_c = 1 is assigned +# todo fix corner cases, like components characteristics == None +from ereuse_devicehub.resources.event.rate.rate import BaseRate + + +class Rate(BaseRate): + """ + Rate all components in Computer + """ + + class Range(Enum): + @classmethod + def from_devicehub(cls, r: Enum): + return getattr(cls, r.name) if r else cls.NONE + + class Appearance(Range): + Z = 0.5 + A = 0.3 + B = 0 + C = -0.2 + D = -0.5 + E = -1.0 + NONE = -0.3 + + class Functionality(Range): + A = 0.4 + B = -0.5 + C = -0.75 + D = -1 + NONE = -0.3 + + def __init__(self) -> None: + super().__init__() + self.RATES = { + # composition: type: (field, compute class) + Processor.t: ('processor', ProcessorRate()), + RamModule.t: ('ram', RamRate()), + DataStorage.t: ('data_storage', DataStorageRate()) + } + + def compute(self, device: Computer, rate: WorkbenchRate): + """ + Compute 'Workbench'Rate computer is a rate (score) ranging from 0 to 4.7 + that represents estimating value of use of desktop and laptop computer components. + + This mutates "rate". + """ + assert isinstance(device, (Desktop, Laptop, Server)) + assert isinstance(rate, WorkbenchRate) + + rate.processor = rate.data_storage = rate.ram = 1 # Init + + # Group cpus, rams, storages and compute their rate + # Treat the same way with HardDrive and SolidStateDrive like (DataStorage) + clause = lambda x: DataStorage.t if isinstance(x, DataStorage) else x.t + c = (c for c in device.components if clause(c) in set(self.RATES.keys())) + for type, components in groupby(sorted(c, key=clause), key=clause): + if type == Processor.t: # ProcessorRate.compute expects only 1 processor + components = next(components) + field, rate_cls = self.RATES[type] # type: str, BaseRate + result = rate_cls.compute(components, rate) + if result: + setattr(rate, field, result) + + rate_components = self.harmonic_mean_rates(rate.processor, rate.data_storage, rate.ram) + rate.appearance = self.Appearance.from_devicehub(rate.appearance_range).value + rate.functionality = self.Functionality.from_devicehub(rate.functionality_range).value + + rate.rating = round(max(rate_components + rate.functionality + rate.appearance, 0), 2) + rate.appearance = round(rate.appearance, 2) + rate.functionality = round(rate.functionality, 2) + rate.processor = round(rate.processor, 2) + rate.ram = round(rate.ram, 2) + rate.data_storage = round(rate.data_storage, 2) + + +class ProcessorRate(BaseRate): + """ + Calculate a ProcessorRate of all Processor devices + """ + # processor.xMin, processor.xMax + PROCESSOR_NORM = 3196.17, 17503.81 + + DEFAULT_CORES = 1 + DEFAULT_SPEED = 1.6 + # In case of i2, i3,.. result penalized. + # Intel(R) Core(TM) i3 CPU 530 @ 2.93GHz, score = 23406.92 but results inan score of 17503. + DEFAULT_SCORE = 4000 + + def compute(self, processor: Processor, rate: WorkbenchRate): + """ Compute processor rate + Obs: cores and speed are possible NULL value + :return: result is a rate (score) of Processor characteristics + """ + # todo for processor_device in processors; more than one processor + cores = processor.cores or self.DEFAULT_CORES + speed = processor.speed or self.DEFAULT_SPEED + # todo fix StopIteration if don't exists BenchmarkProcessor + benchmark_cpu = next(e for e in processor.events if isinstance(e, BenchmarkProcessor)) + benchmark_cpu = benchmark_cpu.rate or self.DEFAULT_SCORE + + # STEP: Fusion components + processor_rate = (benchmark_cpu + speed * 2000 * cores) / 2 # todo magic number! + + # STEP: Normalize values + processor_norm = max(self.norm(processor_rate, *self.PROCESSOR_NORM), 0) + + # STEP: Compute rate/score from every component + # Calculate processor_rate + if processor_norm >= self.CEXP: + processor_rate = self.rate_exp(processor_norm) + if self.CLIN <= processor_norm < self.CLOG: + processor_rate = self.rate_lin(processor_norm) + if processor_norm >= self.CLOG: + processor_rate = self.rate_log(processor_norm) + + assert processor_rate, 'Could not rate processor.' + return processor_rate + + +class RamRate(BaseRate): + """ + Calculate a RamRate of all RamModule devices + """ + # ram.size.xMin; ram.size.xMax + SIZE_NORM = 256, 8192 + RAM_SPEED_NORM = 133, 1333 + # ram.speed.factor + RAM_SPEED_FACTOR = 3.7 + # ram.size.weight; ram.speed.weight; + RAM_WEIGHTS = 0.7, 0.3 + + def compute(self, ram_devices: Iterable[RamModule], rate: WorkbenchRate): + """ + Obs: RamModule.speed is possible NULL value & size != NULL or NOT?? + :return: result is a rate (score) of all RamModule components + """ + size = 0.0 + speed = 0.0 + + # STEP: Filtering, data cleaning and merging of component parts + for ram in ram_devices: + _size = ram.size or 0 + size += _size + if ram.speed: + speed += (ram.speed or 0) * _size + else: + speed += (_size / self.RAM_SPEED_FACTOR) * _size + + # STEP: Fusion components + # To guarantee that there will be no 0/0 + if size: + speed /= size + + # STEP: Normalize values + size_norm = max(self.norm(size, *self.SIZE_NORM), 0) + ram_speed_norm = max(self.norm(speed, *self.RAM_SPEED_NORM), 0) + + # STEP: Compute rate/score from every component + # Calculate size_rate + if self.CEXP <= size_norm < self.CLIN: + size_rate = self.rate_exp(size_norm) + if self.CLIN <= size_norm < self.CLOG: + size_rate = self.rate_lin(size_norm) + if size_norm >= self.CLOG: + size_rate = self.rate_log(size_norm) + # Calculate ram_speed_rate + if self.CEXP <= ram_speed_norm < self.CLIN: + ram_speed_rate = self.rate_exp(ram_speed_norm) + if self.CLIN <= ram_speed_norm < self.CLOG: + ram_speed_rate = self.rate_lin(ram_speed_norm) + if ram_speed_norm >= self.CLOG: + ram_speed_rate = self.rate_log(ram_speed_norm) + + # STEP: Fusion Characteristics + return self.harmonic_mean(self.RAM_WEIGHTS, rates=(size_rate, ram_speed_rate)) + + +class DataStorageRate(BaseRate): + """ + Calculate the rate of all DataStorage devices + """ + # drive.size.xMin; drive.size.xMax + SIZE_NORM = 4, 265000 + READ_SPEED_NORM = 2.7, 109.5 + WRITE_SPEED_NORM = 2, 27.35 + # drive.size.weight; drive.readingSpeed.weight; drive.writingSpeed.weight; + DATA_STORAGE_WEIGHTS = 0.5, 0.25, 0.25 + + def compute(self, data_storage_devices: Iterable[DataStorage], rate: WorkbenchRate): + """ + Obs: size != NULL and 0 value & read_speed and write_speed != NULL + :return: result is a rate (score) of all DataStorage devices + """ + size = 0 + read_speed = 0 + write_speed = 0 + + # STEP: Filtering, data cleaning and merging of component parts + for storage in data_storage_devices: + # todo fix StopIteration if don't exists BenchmarkDataStorage + benchmark = next(e for e in storage.events if isinstance(e, BenchmarkDataStorage)) + # prevent NULL values + _size = storage.size or 0 + size += _size + read_speed += benchmark.read_speed * _size + write_speed += benchmark.write_speed * _size + + # STEP: Fusion components + # Check almost one storage have size, try catch exception 0/0 + if size: + read_speed /= size + write_speed /= size + + # STEP: Normalize values + size_norm = max(self.norm(size, *self.SIZE_NORM), 0) + read_speed_norm = max(self.norm(read_speed, *self.READ_SPEED_NORM), 0) + write_speed_norm = max(self.norm(write_speed, *self.WRITE_SPEED_NORM), 0) + + # STEP: Compute rate/score from every component + # Calculate size_rate + if size_norm >= self.CLOG: + size_rate = self.rate_log(size_norm) + elif self.CLIN <= size_norm < self.CLOG: + size_rate = self.rate_lin(size_norm) + elif self.CEXP <= size_norm < self.CLIN: + size_rate = self.rate_exp(size_norm) + # Calculate read_speed_rate + if read_speed_norm >= self.CLOG: + read_speed_rate = self.rate_log(read_speed_norm) + elif self.CLIN <= read_speed_norm < self.CLOG: + read_speed_rate = self.rate_lin(read_speed_norm) + elif self.CEXP <= read_speed_norm < self.CLIN: + read_speed_rate = self.rate_exp(read_speed_norm) + # write_speed_rate + if write_speed_norm >= self.CLOG: + write_speed_rate = self.rate_log(write_speed_norm) + elif self.CLIN <= write_speed_norm < self.CLOG: + write_speed_rate = self.rate_lin(write_speed_norm) + elif self.CEXP <= write_speed_norm < self.CLIN: + write_speed_rate = self.rate_exp(write_speed_norm) + + # STEP: Fusion Characteristics + return self.harmonic_mean(self.DATA_STORAGE_WEIGHTS, + rates=(size_rate, read_speed_rate, write_speed_rate)) diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 7b36998e..a4172bf7 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -101,7 +101,7 @@ class StepRandom(Step): class Rate(EventWithOneDevice): rating = Integer(validate=Range(*RATE_POSITIVE), dump_only=True, - data_key='ratingValue', + data_key='rating', description='The rating for the content.') software = EnumField(RatingSoftware, dump_only=True, @@ -118,10 +118,6 @@ class IndividualRate(Rate): pass -class AggregateRate(Rate): - ratings = NestedOn(IndividualRate, many=True) - - class PhotoboxRate(IndividualRate): num = Integer(dump_only=True) # todo Image @@ -155,10 +151,6 @@ class ManualRate(IndividualRate): labelling = Boolean(description='Sets if there are labels stuck that should be removed.') -class AppRate(ManualRate): - pass - - class WorkbenchRate(ManualRate): processor = Float() ram = Float() @@ -168,6 +160,16 @@ class WorkbenchRate(ManualRate): 'boot from the network.') +class AggregateRate(Rate): + workbench = NestedOn(WorkbenchRate, dump_only=True) + manual = NestedOn(ManualRate, dump_only=True) + processor = Float(dump_only=True) + ram = Float(dump_only=True) + data_storage = Float(dump_only=True) + graphic_card = Float(dump_only=True) + bios = EnumField(Bios, dump_only=True) + + class Price(EventWithOneDevice): currency = EnumField(Currency, required=True) price = Decimal(places=4, rounding=decimal.ROUND_HALF_EVEN, required=True) @@ -285,7 +287,7 @@ class StressTest(Test): class Benchmark(EventWithOneDevice): - elapsed = TimeDelta(precision=TimeDelta.SECONDS) + elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) class BenchmarkDataStorage(Benchmark): diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py index f6e6bb7f..30f75a0a 100644 --- a/ereuse_devicehub/resources/event/views.py +++ b/ereuse_devicehub/resources/event/views.py @@ -1,4 +1,3 @@ -from contextlib import suppress from distutils.version import StrictVersion from typing import List from uuid import UUID @@ -77,11 +76,9 @@ class SnapshotView(View): snapshot.events |= events # Compute ratings - with suppress(StopIteration): - # todo are we sure we want to have snapshots without rates? - snapshot.events |= next( - e.ratings() for e in events_device if isinstance(e, WorkbenchRate) - ) + for rate in (e for e in events_device if isinstance(e, WorkbenchRate)): + rates = rate.ratings() + snapshot.events |= rates db.session.add(snapshot) db.session.commit() diff --git a/ereuse_devicehub/resources/lot/__init__.py b/ereuse_devicehub/resources/lot/__init__.py index bf70f0e1..b1b74cc5 100644 --- a/ereuse_devicehub/resources/lot/__init__.py +++ b/ereuse_devicehub/resources/lot/__init__.py @@ -15,7 +15,8 @@ class LotDef(Resource): AUTH = True ID_CONVERTER = Converters.uuid - def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None, static_url_path=None, + def __init__(self, app, import_name=__name__.split('.')[0], 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, diff --git a/requirements.txt b/requirements.txt index 074bc613..617ae451 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ click==6.7 click-spinner==0.1.8 colorama==0.3.9 colour==0.1.5 -ereuse-rate==0.0.2 ereuse-utils==0.4.0b9 Flask==1.0.2 Flask-Cors==3.0.6 diff --git a/setup.py b/setup.py index 7302898f..d9f0ae3f 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,6 @@ setup( 'teal>=0.2.0a24', # teal always first 'click', 'click-spinner', - 'ereuse-rate==0.0.2', 'ereuse-utils[Naming]>=0.4b9', 'hashids', 'marshmallow_enum', diff --git a/tests/files/basic.snapshot.yaml b/tests/files/basic.snapshot.yaml index 3adc8d9d..d13d1533 100644 --- a/tests/files/basic.snapshot.yaml +++ b/tests/files/basic.snapshot.yaml @@ -33,3 +33,4 @@ components: events: - type: BenchmarkProcessor rate: 2410 + elapsed: 11 diff --git a/tests/test_device.py b/tests/test_device.py index 475a7f05..ee392400 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -91,7 +91,14 @@ def test_physical_properties(): manufacturer='mr', width=2.0, color=Color()) - pc = Desktop(chassis=ComputerChassis.Tower) + 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')) pc.components.add(c) db.session.add(pc) db.session.commit() @@ -110,6 +117,17 @@ def test_physical_properties(): 'color': Color(), 'depth': None } + assert pc.physical_properties == { + 'model': 'foo', + 'manufacturer': 'bar', + 'serial_number': 'foo-bar', + 'weight': 2.8, + 'width': 1.4, + 'height': 2.1, + 'depth': None, + 'color': Color('LightSeaGreen'), + 'chassis': ComputerChassis.Tower + } @pytest.mark.usefixtures(conftest.app_context.__name__) diff --git a/tests/test_event.py b/tests/test_event.py index 1a1847fa..342e0356 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,5 +1,6 @@ import ipaddress from datetime import timedelta +from typing import Tuple import pytest from flask import current_app as app, g @@ -8,6 +9,7 @@ from teal.enums import Currency, Subdivision from ereuse_devicehub.client import UserClient from ereuse_devicehub.db import db +from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \ RamModule, SolidStateDrive from ereuse_devicehub.resources.enums import ComputerChassis, TestDataStorageLength @@ -175,22 +177,24 @@ def test_update_parent(): assert not benchmark.parent -@pytest.mark.parametrize('event_model', [ - models.ToRepair, - models.Repair, - models.ToPrepare, - models.ReadyToUse, - models.ToPrepare, - models.Prepare, +@pytest.mark.parametrize('event_model_state', [ + (models.ToRepair, states.Physical.ToBeRepaired), + (models.Repair, states.Physical.Repaired), + (models.ToPrepare, states.Physical.Preparing), + (models.ReadyToUse, states.Physical.ReadyToBeUsed), + (models.ToPrepare, states.Physical.Preparing), + (models.Prepare, states.Physical.Prepared) ]) -def test_generic_event(event_model: models.Event, user: UserClient): +def test_generic_event(event_model_state: Tuple[models.Event, states.Trading], user: UserClient): """Tests POSTing all generic events.""" + event_model, state = event_model_state snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot) event = {'type': event_model.t, 'devices': [snapshot['device']['id']]} event, _ = user.post(event, res=models.Event) assert event['devices'][0]['id'] == snapshot['device']['id'] device, _ = user.get(res=Device, item=snapshot['device']['id']) assert device['events'][-1]['id'] == event['id'] + assert device['physical'] == state.name @pytest.mark.usefixtures(conftest.auth_app_context.__name__) @@ -214,6 +218,8 @@ def test_live(): assert live['ip'] == '79.147.10.10' assert live['subdivision'] == 'ES-CA' assert live['country'] == 'ES' + device, _ = client.get(res=Device, item=live['device']['id']) + assert device['physical'] == states.Physical.InUse.name @pytest.mark.xfail(reson='Functionality not developed.') @@ -226,14 +232,15 @@ def test_reserve(user: UserClient): """Performs a reservation and then cancels it.""" -@pytest.mark.parametrize('event_model', [ - models.Sell, - models.Donate, - models.Rent, - models.DisposeProduct +@pytest.mark.parametrize('event_model_state', [ + (models.Sell, states.Trading.Sold), + (models.Donate, states.Trading.Donated), + (models.Rent, states.Trading.Renting), + (models.DisposeProduct, states.Trading.ProductDisposed) ]) -def test_trade(event_model: models.Event, user: UserClient): - """Tests POSTing all generic events.""" +def test_trade(event_model_state: Tuple[models.Event, states.Trading], user: UserClient): + """Tests POSTing all Trade events.""" + event_model, state = event_model_state snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot) event = { 'type': event_model.t, @@ -246,6 +253,7 @@ def test_trade(event_model: models.Event, user: UserClient): assert event['devices'][0]['id'] == snapshot['device']['id'] device, _ = user.get(res=Device, item=snapshot['device']['id']) assert device['events'][-1]['id'] == event['id'] + assert device['trading'] == state.name @pytest.mark.xfail(reson='Develop migrate') @@ -259,6 +267,7 @@ def test_price_custom(): chassis=ComputerChassis.Docking) price = models.Price(price=25.25, currency=Currency.EUR) price.device = computer + assert computer.price == price db.session.add(computer) db.session.commit() @@ -268,3 +277,6 @@ def test_price_custom(): assert p['device']['id'] == price.device.id == computer.id assert p['price'] == 25.25 assert p['currency'] == Currency.EUR.name == 'EUR' + + c, _ = client.get(res=Device, item=computer.id) + assert c['price']['id'] == p['id'] diff --git a/tests/test_rate.py b/tests/test_rate.py index ff196fce..699975cf 100644 --- a/tests/test_rate.py +++ b/tests/test_rate.py @@ -3,11 +3,13 @@ from distutils.version import StrictVersion import pytest from ereuse_devicehub.db import db -from ereuse_devicehub.resources.device.models import Computer, Desktop -from ereuse_devicehub.resources.enums import Bios, ComputerChassis, ImageMimeTypes, Orientation, \ - RatingSoftware -from ereuse_devicehub.resources.event.models import PhotoboxRate, WorkbenchRate -from ereuse_devicehub.resources.image.models import Image, ImageList +from ereuse_devicehub.resources.device.models import Computer, Desktop, HardDrive, Processor, \ + RamModule +from ereuse_devicehub.resources.enums import AppearanceRange, Bios, ComputerChassis, \ + FunctionalityRange, RatingSoftware +from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkDataStorage, \ + BenchmarkProcessor, EreusePrice, WorkbenchRate +from ereuse_devicehub.resources.event.rate import main from tests import conftest @@ -26,17 +28,63 @@ def test_workbench_rate_db(): db.session.commit() -@pytest.mark.usefixtures(conftest.auth_app_context.__name__) -def test_photobox_rate_db(): - pc = Desktop(serial_number='24', chassis=ComputerChassis.Tower) - image = Image(name='foo', - content=b'123', - file_format=ImageMimeTypes.jpg, - orientation=Orientation.Horizontal, - image_list=ImageList(device=pc)) - rate = PhotoboxRate(image=image, - software=RatingSoftware.ECost, - version=StrictVersion('1.0'), - device=pc) - db.session.add(rate) - db.session.commit() +@pytest.mark.xfail(reason='AggreagteRate only takes data from WorkbenchRate as for now') +def test_rate_workbench_then_manual(): + """Checks that a new AggregateRate is generated with a new rate + value when a ManualRate is performed after performing a + WorkbenchRate. + + The new AggregateRate needs to be computed by the values of + the WorkbenchRate + new values from ManualRate. + """ + pass + + +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_rate(): + """Test generating an AggregateRate for a given PC / components / + WorkbenchRate ensuring results and relationships between + pc - rate - workbenchRate - price. + """ + rate = WorkbenchRate( + appearance_range=AppearanceRange.A, + functionality_range=FunctionalityRange.A + ) + pc = Desktop() + hdd = HardDrive(size=476940) + hdd.events_one.add(BenchmarkDataStorage(read_speed=126, write_speed=29.8)) + cpu = Processor(cores=2, speed=3.4) + cpu.events_one.add(BenchmarkProcessor(rate=27136.44)) + pc.components |= { + hdd, + RamModule(size=4096, speed=1600), + RamModule(size=2048, speed=1067), + cpu + } + rate.device = pc + events = main.main(rate, RatingSoftware.ECost, StrictVersion('1.0')) + price = next(e for e in events if isinstance(e, EreusePrice)) + assert price.price == 92.2 + assert price.retailer.standard.amount == 40.97 + assert price.platform.standard.amount == 18.84 + assert price.refurbisher.standard.amount == 32.38 + assert price.price >= price.retailer.standard.amount \ + + price.platform.standard.amount \ + + price.refurbisher.standard.amount + assert price.retailer.warranty2.amount == 55.30 + assert price.platform.warranty2.amount == 25.43 + assert price.refurbisher.warranty2.amount == 43.72 + assert price.warranty2 == 124.45 + # Checks relationships + workbench_rate = next(e for e in events if isinstance(e, WorkbenchRate)) + aggregate_rate = next(e for e in events if isinstance(e, AggregateRate)) + assert price.rating == aggregate_rate + assert aggregate_rate.workbench == workbench_rate + assert aggregate_rate.rating == workbench_rate.rating == 4.61 + assert aggregate_rate.software == workbench_rate.software == RatingSoftware.ECost + assert aggregate_rate.version == StrictVersion('1.0') + assert aggregate_rate.appearance == workbench_rate.appearance + assert aggregate_rate.functionality == workbench_rate.functionality + assert aggregate_rate.rating_range == workbench_rate.rating_range + assert cpu.rate == pc.rate == hdd.rate == aggregate_rate + assert cpu.price == pc.price == aggregate_rate.price == hdd.price == price diff --git a/tests/test_rate_workbench_v1.py b/tests/test_rate_workbench_v1.py new file mode 100644 index 00000000..a078e797 --- /dev/null +++ b/tests/test_rate_workbench_v1.py @@ -0,0 +1,417 @@ +""" +Tests of compute rating for every component in a Device +Rates test done: + -DataStorage + -RamModule + -Processor + +Excluded cases in tests + +- No Processor +- + +""" + +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.event.models import BenchmarkDataStorage, BenchmarkProcessor, \ + WorkbenchRate +from ereuse_devicehub.resources.event.rate.workbench.v1_0 import DataStorageRate, ProcessorRate, \ + RamRate, Rate + + +def test_rate_data_storage_rate(): + """ + Test to check if compute data storage rate have same value than previous score version; + id = pc_1193, pc_1201, pc_79, pc_798 + """ + + hdd_1969 = HardDrive(size=476940) + hdd_1969.events_one.add(BenchmarkDataStorage(read_speed=126, write_speed=29.8)) + + data_storage_rate = DataStorageRate().compute([hdd_1969], WorkbenchRate()) + + assert round(data_storage_rate, 2) == 4.02, 'DataStorageRate returns incorrect value(rate)' + + hdd_3054 = HardDrive(size=476940) + hdd_3054.events_one.add(BenchmarkDataStorage(read_speed=158, write_speed=34.7)) + + # calculate DataStorage Rate + data_storage_rate = DataStorageRate().compute([hdd_3054], WorkbenchRate()) + + assert round(data_storage_rate, 2) == 4.07, 'DataStorageRate returns incorrect value(rate)' + + hdd_81 = HardDrive(size=76319) + hdd_81.events_one.add(BenchmarkDataStorage(read_speed=72.2, write_speed=24.3)) + + data_storage_rate = DataStorageRate().compute([hdd_81], WorkbenchRate()) + + assert round(data_storage_rate, 2) == 2.61, 'DataStorageRate returns incorrect value(rate)' + + hdd_1556 = HardDrive(size=152587) + hdd_1556.events_one.add(BenchmarkDataStorage(read_speed=78.1, write_speed=24.4)) + + data_storage_rate = DataStorageRate().compute([hdd_1556], WorkbenchRate()) + + assert round(data_storage_rate, 2) == 3.70, 'DataStorageRate returns incorrect value(rate)' + + +def test_rate_data_storage_size_is_null(): + """ + Test where input DataStorage.size = NULL, BenchmarkDataStorage.read_speed = 0, + BenchmarkDataStorage.write_speed = 0 is like no DataStorage has been detected; + id = pc_2992 + """ + + hdd_null = HardDrive(size=None) + hdd_null.events_one.add(BenchmarkDataStorage(read_speed=0, write_speed=0)) + + data_storage_rate = DataStorageRate().compute([hdd_null], WorkbenchRate()) + assert data_storage_rate is None + + +def test_rate_no_data_storage(): + """ + Test without data storage devices + """ + hdd_null = HardDrive() + hdd_null.events_one.add(BenchmarkDataStorage(read_speed=0, write_speed=0)) + data_storage_rate = DataStorageRate().compute([hdd_null], WorkbenchRate()) + assert data_storage_rate is None + + +# RAM MODULE DEVICE TEST + + +def test_rate_ram_rate(): + """ + Test to check if compute ram rate have same value than previous score version + only with 1 RamModule; id = pc_1201 + """ + + ram1 = RamModule(size=2048, speed=1333) + + ram_rate = RamRate().compute([ram1], WorkbenchRate()) + + assert round(ram_rate, 2) == 2.02, 'RamRate returns incorrect value(rate)' + + +def test_rate_ram_rate_2modules(): + """ + Test to check if compute ram rate have same value than previous score version + with 2 RamModule; id = pc_1193 + """ + + ram1 = RamModule(size=4096, speed=1600) + ram2 = RamModule(size=2048, speed=1067) + + ram_rate = RamRate().compute([ram1, ram2], WorkbenchRate()) + + assert round(ram_rate, 2) == 3.79, 'RamRate returns incorrect value(rate)' + + +def test_rate_ram_rate_4modules(): + """ + Test to check if compute ram rate have same value than previous score version + with 2 RamModule; id = pc_79 + """ + + ram1 = RamModule(size=512, speed=667) + ram2 = RamModule(size=512, speed=800) + ram3 = RamModule(size=512, speed=667) + ram4 = RamModule(size=512, speed=533) + + ram_rate = RamRate().compute([ram1, ram2, ram3, ram4], WorkbenchRate()) + + assert round(ram_rate, 2) == 1.99, 'RamRate returns incorrect value(rate)' + + +def test_rate_ram_module_size_is_0(): + """ + Test where input data RamModule.size = 0; is like no RamModule has been detected; id = pc_798 + """ + + ram0 = RamModule(size=0, speed=888) + + ram_rate = RamRate().compute([ram0], WorkbenchRate()) + assert ram_rate is None + + +def test_rate_ram_speed_is_null(): + """ + Test where RamModule.speed is NULL (not detected) but has size. + Pc ID = 795(1542), 745(1535), 804(1549) + """ + + ram0 = RamModule(size=2048, speed=None) + + ram_rate = RamRate().compute([ram0], WorkbenchRate()) + + assert round(ram_rate, 2) == 1.85, 'RamRate returns incorrect value(rate)' + + ram0 = RamModule(size=1024, speed=None) + + ram_rate = RamRate().compute([ram0], WorkbenchRate()) + + assert round(ram_rate, 2) == 1.25, 'RamRate returns incorrect value(rate)' + + +def test_rate_no_ram_module(): + """ + Test without RamModule + """ + ram0 = RamModule() + + ram_rate = RamRate().compute([ram0], WorkbenchRate()) + assert ram_rate is None + + +# PROCESSOR DEVICE TEST + +def test_rate_processor_rate(): + """ + Test to check if compute processor rate have same value than previous score version + only with 1 core; id = 79 + """ + + cpu = Processor(cores=1, speed=1.6) + # add score processor benchmark + cpu.events_one.add(BenchmarkProcessor(rate=3192.34)) + + processor_rate = ProcessorRate().compute(cpu, WorkbenchRate()) + + assert processor_rate == 1, 'ProcessorRate returns incorrect value(rate)' + + +def test_rate_processor_rate_2cores(): + """ + Test to check if compute processor rate have same value than previous score version + with 2 cores; id = pc_1193, pc_1201 + """ + + cpu = Processor(cores=2, speed=3.4) + # add score processor benchmark + cpu.events_one.add(BenchmarkProcessor(rate=27136.44)) + + processor_rate = ProcessorRate().compute(cpu, WorkbenchRate()) + + assert round(processor_rate, 2) == 3.95, 'ProcessorRate returns incorrect value(rate)' + + cpu = Processor(cores=2, speed=3.3) + cpu.events_one.add(BenchmarkProcessor(rate=26339.48)) + + processor_rate = ProcessorRate().compute(cpu, WorkbenchRate()) + + assert round(processor_rate, 2) == 3.93, 'ProcessorRate returns incorrect value(rate)' + + +@pytest.mark.xfail(reason='Debug test') +def test_rate_processor_with_null_cores(): + """ + Test with processor device have null number of cores + """ + cpu = Processor(cores=None, speed=3.3) + cpu.events_one.add(BenchmarkProcessor(rate=0)) + + processor_rate = ProcessorRate().compute(cpu, WorkbenchRate()) + + assert processor_rate == 1, 'ProcessorRate returns incorrect value(rate)' + + +@pytest.mark.xfail(reason='Debug test') +def test_rate_processor_with_null_speed(): + """ + Test with processor device have null speed value + """ + cpu = Processor(cores=1, speed=None) + cpu.events_one.add(BenchmarkProcessor(rate=0)) + + processor_rate = ProcessorRate().compute(cpu, WorkbenchRate()) + + assert processor_rate == 1.06, 'ProcessorRate returns incorrect value(rate)' + + +def test_rate_computer_rate(): + """ Test rate v1 + + pc_1193 = Computer() + price = 92.2 + # add components characteristics of pc with id = 1193 + hdd_1969 = HardDrive(size=476940) + hdd_1969.events_one.add(BenchmarkDataStorage(read_speed=126, write_speed=29.8)) + ram1 = RamModule(size=4096, speed=1600) + ram2 = RamModule(size=2048, speed=1067) + cpu = Processor(cores=2, speed=3.4) + cpu.events_one.add(BenchmarkProcessor(rate=27136.44)) + pc_1193.components.add(hdd_1969, ram1, ram2, cpu) + # add functionality and appearance range + rate_pc_1193 = WorkbenchRate(appearance_range=AppearanceRange.A, functionality_range=FunctionalityRange.A) + # add component rate + HDD_rate = 4.02 + RAM_rate = 3.79 + Processor_rate = 3.95 + Rating = 4.61 + + pc_1201 = Computer() + price = 69.6 + hdd_3054 = HardDrive(size=476940) + hdd_3054.events_one.add(BenchmarkDataStorage(read_speed=158, write_speed=34.7)) + ram1 = RamModule(size=2048, speed=1333) + cpu = Processor(cores=2, speed=3.3) + cpu.events_one.add(BenchmarkProcessor(rate=26339.48)) + pc_1201.components.add(hdd_3054, ram1, cpu) + # add functionality and appearance range + rate_pc_1201 = WorkbenchRate(appearance_range=AppearanceRange.B, functionality_range=FunctionalityRange.A) + # add component rate + HDD_rate = 4.07 + RAM_rate = 2.02 + Processor_rate = 3.93 + Rating = 3.48 + + pc_79 = Computer() + price = VeryLow + hdd_81 = HardDrive(size=76319) + hdd_81.events_one.add(BenchmarkDataStorage(read_speed=72.2, write_speed=24.3)) + ram1 = RamModule(size=512, speed=667) + ram2 = RamModule(size=512, speed=800) + ram3 = RamModule(size=512, speed=667) + ram4 = RamModule(size=512, speed=533) + cpu = Processor(cores=1, speed=1.6) + cpu.events_one.add(BenchmarkProcessor(rate=3192.34)) + pc_79.components.add(hdd_81, ram1, ram2, ram3, ram4, cpu) + # add functionality and appearance range + rate_pc_79 = WorkbenchRate(appearance_range=AppearanceRange.C, functionality_range=FunctionalityRange.A) + # add component rate + HDD_rate = 2.61 + RAM_rate = 1.99 + Processor_rate = 1 + Rating = 1.58 + + pc_798 = Computer() + price = 50 + hdd_1556 = HardDrive(size=152587) + hdd_1556.events_one.add(BenchmarkDataStorage(read_speed=78.1, write_speed=24.4)) + ram0 = RamModule(size=0, speed=None) + cpu = Processor(cores=2, speed=2.5) + cpu.events_one.add(BenchmarkProcessor(rate=9974.3)) + pc_798.components.add(hdd_1556, ram0, cpu) + # add functionality and appearance range + rate_pc_798 = WorkbenchRate(appearance_range=AppearanceRange.B, functionality_range=FunctionalityRange.A) + # add component rate + HDD_rate = 3.7 + RAM_rate = 1 + Processor_rate = 4.09 + Rating = 2.5 + """ + + # Create a new Computer with components characteristics of pc with id = 1193 + pc_test = Desktop() + 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) + cpu.events_one.add(BenchmarkProcessor(rate=27136.44)) + pc_test.components |= { + data_storage, + RamModule(size=4096, speed=1600), + RamModule(size=2048, speed=1067), + cpu + } + # add functionality and appearance range + rate_pc = WorkbenchRate(appearance_range=AppearanceRange.A, + functionality_range=FunctionalityRange.A) + # Compute all components rates and general rating + Rate().compute(pc_test, rate_pc) + + assert round(rate_pc.ram, 2) == 3.79 + + assert round(rate_pc.data_storage, 2) == 4.02 + + assert round(rate_pc.processor, 2) == 3.95 + + assert round(rate_pc.rating, 2) == 4.61 + + # Create a new Computer with components characteristics of pc with id = 1201 + pc_test = Desktop() + 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) + cpu.events_one.add(BenchmarkProcessor(rate=26339.48)) + pc_test.components |= { + data_storage, + RamModule(size=2048, speed=1333), + cpu + } + # add functionality and appearance range + rate_pc = WorkbenchRate(appearance_range=AppearanceRange.B, + functionality_range=FunctionalityRange.A) + # Compute all components rates and general rating + Rate().compute(pc_test, rate_pc) + + assert round(rate_pc.ram, 2) == 2.02 + + assert round(rate_pc.data_storage, 2) == 4.07 + + assert round(rate_pc.processor, 2) == 3.93 + + assert round(rate_pc.rating, 2) == 3.48 + + # Create a new Computer with components characteristics of pc with id = 79 + pc_test = Desktop() + 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) + cpu.events_one.add(BenchmarkProcessor(rate=3192.34)) + pc_test.components |= { + data_storage, + RamModule(size=512, speed=667), + RamModule(size=512, speed=800), + RamModule(size=512, speed=667), + RamModule(size=512, speed=533), + cpu + } + # add functionality and appearance range + rate_pc = WorkbenchRate(appearance_range=AppearanceRange.C, + functionality_range=FunctionalityRange.A) + # Compute all components rates and general rating + Rate().compute(pc_test, rate_pc) + + assert round(rate_pc.ram, 2) == 1.99 + + assert round(rate_pc.data_storage, 2) == 2.61 + + assert round(rate_pc.processor, 2) == 1 + + assert round(rate_pc.rating, 2) == 1.58 + + # Create a new Computer with components characteristics of pc with id = 798 + pc_test = Desktop() + 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) + cpu.events_one.add(BenchmarkProcessor(rate=9974.3)) + pc_test.components |= { + data_storage, + RamModule(size=0, speed=None), + cpu + } + # add functionality and appearance range + rate_pc = WorkbenchRate(appearance_range=AppearanceRange.B, + functionality_range=FunctionalityRange.A) + # Compute all components rates and general rating + Rate().compute(pc_test, rate_pc) + + assert round(rate_pc.ram, 2) == 1 + + assert round(rate_pc.data_storage, 2) == 3.7 + + assert round(rate_pc.processor, 2) == 4.09 + + assert round(rate_pc.rating, 2) == 2.5 + + +@pytest.mark.xfail(reason='Data Storage rate actually requires a DSSBenchmark') +def test_rate_computer_with_data_storage_without_benchmark(): + """For example if the data storage was introduced manually + or comes from an old version without benchmark.""" diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 54c23b47..85ff8697 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -1,9 +1,9 @@ from datetime import datetime, timedelta, timezone -from distutils.version import StrictVersion from typing import List, Tuple from uuid import uuid4 import pytest +from boltons import urlutils from teal.db import UniqueViolation from teal.marshmallow import ValidationError @@ -13,8 +13,7 @@ from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.device import models as m from ereuse_devicehub.resources.device.exceptions import NeedsId from ereuse_devicehub.resources.device.sync import MismatchBetweenTagsAndHid -from ereuse_devicehub.resources.enums import Bios, ComputerChassis, RatingSoftware, \ - SnapshotSoftware +from ereuse_devicehub.resources.enums import ComputerChassis, SnapshotSoftware from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkProcessor, \ EraseSectors, Event, Snapshot, SnapshotRequest, WorkbenchRate from ereuse_devicehub.resources.tag import Tag @@ -37,21 +36,11 @@ def test_snapshot_model(): elapsed=timedelta(seconds=25)) snapshot.device = device snapshot.request = SnapshotRequest(request={'foo': 'bar'}) - snapshot.events.add(WorkbenchRate(processor=0.1, - ram=1.0, - bios=Bios.A, - labelling=False, - graphic_card=0.1, - data_storage=4.1, - software=RatingSoftware.ECost, - version=StrictVersion('1.0'), - device=device)) db.session.add(snapshot) db.session.commit() device = m.Desktop.query.one() # type: m.Desktop - e1, e2 = device.events + e1 = device.events[0] assert isinstance(e1, Snapshot), 'Creation order must be preserved: 1. snapshot, 2. WR' - assert isinstance(e2, WorkbenchRate) db.session.delete(device) db.session.commit() assert Snapshot.query.one_or_none() is None @@ -59,6 +48,8 @@ def test_snapshot_model(): assert User.query.one() is not None assert m.Desktop.query.one_or_none() is None assert m.Device.query.one_or_none() is None + # Check properties + assert device.url == urlutils.URL('http://localhost/devices/1') def test_snapshot_schema(app: Devicehub): @@ -321,27 +312,37 @@ def test_erase(user: UserClient): assert step['type'] == 'StepZero' assert step['error'] is False assert 'num' not in step + assert storage['privacy'] == erasure['device']['privacy'] == 'EraseSectors' + + # Let's try a second erasure with an error + s['uuid'] = uuid4() + s['components'][0]['events'][0]['error'] = True + snapshot, _ = user.post(s, res=Snapshot) + assert snapshot['components'][0]['hid'] == 'c1mr-c1s-c1ml' + assert snapshot['components'][0]['privacy'] == 'EraseSectorsError' def test_snapshot_computer_monitor(user: UserClient): s = file('computer-monitor.snapshot') - snapshot_and_check(user, s, event_types=('AppRate',)) + snapshot_and_check(user, s, event_types=('ManualRate',)) + # todo check that ManualRate has generated an AggregateRate def test_snapshot_mobile_smartphone(user: UserClient): s = file('smartphone.snapshot') - snapshot_and_check(user, s, event_types=('AppRate',)) + snapshot_and_check(user, s, event_types=('ManualRate',)) + # todo check that ManualRate has generated an AggregateRate +@pytest.mark.xfail(reason='Test not developed') def test_snapshot_components_none(): """ Tests that a snapshot without components does not remove them from the computer. """ - # todo test - pass +@pytest.mark.xfail(reason='Test not developed') def test_snapshot_components_empty(): """ Tests that a snapshot whose components are an empty list remove diff --git a/tests/test_workbench.py b/tests/test_workbench.py index 116882fb..a2a3c8ef 100644 --- a/tests/test_workbench.py +++ b/tests/test_workbench.py @@ -27,7 +27,7 @@ def test_workbench_server_condensed(user: UserClient): file('workbench-server-3.erase'), file('workbench-server-4.install') )) - s['components'][5]['events'] = [file('workbench-server-3.erase')] + s['components'][5]['events'].append(file('workbench-server-3.erase')) # Create tags for t in s['device']['tags']: user.post({'id': t['id']}, res=Tag) @@ -35,7 +35,7 @@ def test_workbench_server_condensed(user: UserClient): snapshot, _ = user.post(res=em.Snapshot, data=s) events = snapshot['events'] assert {(event['type'], event['device']) for event in events} == { - # todo missing Rate event aggregating the rates + ('AggregateRate', 1), ('WorkbenchRate', 1), ('BenchmarkProcessorSysbench', 5), ('StressTest', 1), @@ -45,10 +45,24 @@ def test_workbench_server_condensed(user: UserClient): ('Install', 6), ('EraseSectors', 7), ('BenchmarkDataStorage', 6), + ('BenchmarkDataStorage', 7), ('TestDataStorage', 6) } assert snapshot['closed'] assert not snapshot['error'] + device, _ = user.get(res=Device, item=snapshot['device']['id']) + assert device['dataStorageSize'] == 1100 + assert device['chassis'] == 'Tower' + assert device['hid'] == 'd1mr-d1s-d1ml' + assert device['graphicCardModel'] == device['components'][0]['model'] == 'gc1-1ml' + assert device['networkSpeeds'] == [1000, 58] + assert device['processorModel'] == device['components'][3]['model'] == 'p1-1ml' + assert device['ramSize'] == 2048, 'There are 3 RAM: 2 x 1024 and 1 None sizes' + assert device['rate']['closed'] + assert not device['rate']['error'] + assert device['rate']['rating'] == 0 + assert device['rate']['workbench'] + assert device['tags'][0]['id'] == 'tag1' @pytest.mark.xfail(reason='Functionality not yet developed.') @@ -122,8 +136,9 @@ def test_workbench_server_phases(user: UserClient): def test_real_hp_11(user: UserClient): s = file('real-hp.snapshot.11') snapshot, _ = user.post(res=em.Snapshot, data=s) - assert snapshot['device']['hid'] == 'hewlett-packard-czc0408yjg-hp_compaq_8100_elite_sff' - assert snapshot['device']['chassis'] == 'Tower' + pc = snapshot['device'] + assert pc['hid'] == 'hewlett-packard-czc0408yjg-hp_compaq_8100_elite_sff' + assert pc['chassis'] == 'Tower' assert set(e['type'] for e in snapshot['events']) == { 'BenchmarkDataStorage', 'BenchmarkProcessor', @@ -133,6 +148,10 @@ def test_real_hp_11(user: UserClient): 'StressTest' } assert len(list(e['type'] for e in snapshot['events'])) == 6 + 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 def test_real_toshiba_11(user: UserClient): @@ -140,7 +159,7 @@ def test_real_toshiba_11(user: UserClient): snapshot, _ = user.post(res=em.Snapshot, data=s) -def test_real_eee_1001pxd(user: UserClient): +def test_snapshot_real_eee_1001pxd(user: UserClient): """ Checks the values of the device, components, events and their relationships of a real pc. @@ -155,6 +174,7 @@ def test_real_eee_1001pxd(user: UserClient): assert pc['manufacturer'] == 'asustek computer inc.' 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' components = snapshot['components'] wifi = components[0] assert wifi['hid'] == 'qualcomm_atheros-74_2f_68_8b_fd_c8-ar9285_wireless_network_adapter' @@ -170,7 +190,7 @@ def test_real_eee_1001pxd(user: UserClient): assert cpu['threads'] == 1 assert cpu['speed'] == 1.667 assert 'hid' not in cpu - assert cpu['model'] == 'intel atom cpu n455 @ 1.66ghz' + assert pc['processorModel'] == cpu['model'] == 'intel atom cpu n455 @ 1.66ghz' cpu, _ = user.get(res=Device, item=cpu['id']) events = cpu['events'] sysbench = next(e for e in events if e['type'] == em.BenchmarkProcessorSysbench.t) @@ -204,6 +224,7 @@ def test_real_eee_1001pxd(user: UserClient): ram = components[6] assert ram['interface'] == 'DDR2' assert ram['speed'] == 667 + assert pc['ramSize'] == ram['size'] == 1024 hdd = components[7] assert hdd['type'] == 'HardDrive' assert hdd['hid'] == 'hitachi-e2024242cv86hj-hts54322' @@ -223,6 +244,7 @@ def test_real_eee_1001pxd(user: UserClient): assert erase['startTime'] assert erase['zeros'] is False assert erase['error'] is False + assert hdd['privacy'] == 'EraseBasic' mother = components[8] assert mother['hid'] == 'asustek_computer_inc-eee0123456789-1001pxd' @@ -243,6 +265,11 @@ def test_real_eee_1000h(user: UserClient): snapshot, _ = user.post(res=em.Snapshot, data=s) +@pytest.mark.xfail(reason='We do not have a snapshot file to use') +def test_real_full_with_workbench_rate(user: UserClient): + pass + + SNAPSHOTS_NEED_ID = { 'box-xavier.snapshot.json', 'custom.lshw.snapshot.json',