From 3cc510831c16fec1653a81a52b1995a31565766f Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sat, 13 Oct 2018 14:53:46 +0200 Subject: [PATCH 1/8] Add states and quick-access properties on Device; merge in ereuse-rate --- docs/states.puml | 14 +- .../files/computer-monitor.snapshot.yaml | 2 +- .../dummy/files/smartphone.snapshot.yaml | 2 +- .../files/workbench-server-1.snapshot.yaml | 25 +- ereuse_devicehub/resources/device/models.py | 107 ++++- ereuse_devicehub/resources/device/models.pyi | 50 ++- ereuse_devicehub/resources/device/schemas.py | 26 +- ereuse_devicehub/resources/device/states.py | 30 ++ ereuse_devicehub/resources/enums.py | 18 + ereuse_devicehub/resources/event/__init__.py | 7 +- ereuse_devicehub/resources/event/models.py | 112 +++-- ereuse_devicehub/resources/event/models.pyi | 84 ++-- .../resources/event/rate/__init__.py | 0 ereuse_devicehub/resources/event/rate/main.py | 78 ++++ ereuse_devicehub/resources/event/rate/rate.py | 54 +++ .../event/rate/workbench/__init__.py | 0 .../resources/event/rate/workbench/v1_0.py | 253 +++++++++++ ereuse_devicehub/resources/event/schemas.py | 22 +- ereuse_devicehub/resources/event/views.py | 9 +- ereuse_devicehub/resources/lot/__init__.py | 3 +- requirements.txt | 1 - setup.py | 1 - tests/files/basic.snapshot.yaml | 1 + tests/test_device.py | 20 +- tests/test_event.py | 42 +- tests/test_rate.py | 86 +++- tests/test_rate_workbench_v1.py | 417 ++++++++++++++++++ tests/test_snapshot.py | 37 +- tests/test_workbench.py | 39 +- 29 files changed, 1330 insertions(+), 210 deletions(-) create mode 100644 ereuse_devicehub/resources/device/states.py create mode 100644 ereuse_devicehub/resources/event/rate/__init__.py create mode 100644 ereuse_devicehub/resources/event/rate/main.py create mode 100644 ereuse_devicehub/resources/event/rate/rate.py create mode 100644 ereuse_devicehub/resources/event/rate/workbench/__init__.py create mode 100644 ereuse_devicehub/resources/event/rate/workbench/v1_0.py create mode 100644 tests/test_rate_workbench_v1.py 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', From 10c1a3f37d5460d2d99916933a33b80cc2eed9f5 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sun, 14 Oct 2018 11:37:10 +0200 Subject: [PATCH 2/8] Bugfixes with rate --- ereuse_devicehub/resources/event/models.py | 16 +++++++++++++++- ereuse_devicehub/resources/event/models.pyi | 16 +++++++++++++++- ereuse_devicehub/resources/event/schemas.py | 13 +++++++------ tests/files/basic.snapshot.yaml | 2 +- tests/test_lot.py | 2 +- tests/test_rate.py | 2 +- 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 3bbc44d0..758c39ea 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -388,8 +388,17 @@ class IndividualRate(Rate): class ManualRate(IndividualRate): id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) labelling = Column(Boolean) + labelling.comment = """Sets if there are labels stuck that should + be removed. + """ appearance_range = Column(DBEnum(AppearanceRange)) + appearance_range.comment = """Grades the imperfections that + aesthetically affect the device, but not its usage. + """ functionality_range = Column(DBEnum(FunctionalityRange)) + functionality_range.comment = """Grades the defects of a device + affecting usage. + """ class WorkbenchRate(ManualRate): @@ -400,7 +409,12 @@ class WorkbenchRate(ManualRate): check_range('data_storage', *RATE_POSITIVE)) graphic_card = Column(Float(decimal_return_scale=2), check_range('graphic_card', *RATE_POSITIVE)) - bios = Column(DBEnum(Bios)) + bios = Column(Float(decimal_return_scale=2), + check_range('bios', *RATE_POSITIVE)) + bios_range = Column(DBEnum(Bios)) + bios_range.comment = """How difficult it has been to set the bios + to boot from the network. + """ # todo ensure for WorkbenchRate version and software are not None when inserting them diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index 8c68cf8e..b11d5e6e 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -203,6 +203,11 @@ class AggregateRate(Rate): class ManualRate(IndividualRate): + labelling = ... # type: Column + appearance_range = ... # type: Column + functionality_range = ... # type: Column + aggregate_rate_manual = ... #type: relationship + def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.labelling = ... # type: bool @@ -212,13 +217,22 @@ class ManualRate(IndividualRate): class WorkbenchRate(ManualRate): + processor = ... # type: Column + ram = ... # type: Column + data_storage = ... # type: Column + graphic_card = ... # type: Column + bios_range = ... # type: Column + bios = ... # type: Column + aggregate_rate_workbench = ... #type: Column + 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.bios = ... # type: Bios + self.bios_range = ... # type: Bios + self.bios = ... # type: float self.aggregate_rate_workbench = ... #type: AggregateRate def ratings(self) -> Set[Rate]: diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index a4172bf7..4007b32d 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -142,13 +142,12 @@ class ManualRate(IndividualRate): appearance_range = EnumField(AppearanceRange, required=True, data_key='appearanceRange', - description='Grades the imperfections that aesthetically ' - 'affect the device, but not its usage.') + description=m.ManualRate.appearance_range.comment) functionality_range = EnumField(FunctionalityRange, required=True, data_key='functionalityRange', - description='Grades the defects of a device affecting usage.') - labelling = Boolean(description='Sets if there are labels stuck that should be removed.') + description=m.ManualRate.functionality_range.comment) + labelling = Boolean(description=m.ManualRate.labelling.comment) class WorkbenchRate(ManualRate): @@ -156,8 +155,10 @@ class WorkbenchRate(ManualRate): 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.') + bios = Float() + bios_range = EnumField(Bios, + description=m.WorkbenchRate.bios_range.comment, + data_key='biosRange') class AggregateRate(Rate): diff --git a/tests/files/basic.snapshot.yaml b/tests/files/basic.snapshot.yaml index d13d1533..63e2f715 100644 --- a/tests/files/basic.snapshot.yaml +++ b/tests/files/basic.snapshot.yaml @@ -14,7 +14,7 @@ device: appearanceRange: A functionalityRange: B labelling: True - bios: B + biosRange: B components: - type: GraphicCard serialNumber: gc1s diff --git a/tests/test_lot.py b/tests/test_lot.py index 372330dc..8749ed96 100644 --- a/tests/test_lot.py +++ b/tests/test_lot.py @@ -254,7 +254,7 @@ def test_post_get_lot(user: UserClient): assert not l['children'] -def test_post_add_children_view_ui_tree_normal(user: UserClient): +def test_lot_post_add_children_view_ui_tree_normal(user: UserClient): """Tests adding children lots to a lot through the view and GETting the results.""" parent, _ = user.post(({'name': 'Parent'}), res=Lot) diff --git a/tests/test_rate.py b/tests/test_rate.py index 699975cf..bf1cee90 100644 --- a/tests/test_rate.py +++ b/tests/test_rate.py @@ -17,7 +17,7 @@ from tests import conftest def test_workbench_rate_db(): rate = WorkbenchRate(processor=0.1, ram=1.0, - bios=Bios.A, + bios_range=Bios.A, labelling=False, graphic_card=0.1, data_storage=4.1, From 9cffa921254f4501c9e9c14604eafd310d65ce8a Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sun, 14 Oct 2018 20:10:52 +0200 Subject: [PATCH 3/8] Update description of Rate and Device; remove PhotoboxRate schemas --- ereuse_devicehub/resources/device/models.py | 11 ++--- ereuse_devicehub/resources/device/schemas.py | 13 +++--- ereuse_devicehub/resources/event/__init__.py | 10 ----- ereuse_devicehub/resources/event/models.py | 33 ++++++++++---- ereuse_devicehub/resources/event/models.pyi | 2 + ereuse_devicehub/resources/event/schemas.py | 46 +++++++------------- tests/test_basic.py | 2 +- 7 files changed, 57 insertions(+), 60 deletions(-) diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index e5d6179f..9e47b557 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -103,22 +103,23 @@ class Device(Thing): @property def rate(self): - """Gets the last aggregate rate.""" + """The last AggregateRate of the device.""" 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.""" + """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.""" + """The actual trading state, or None if no Trade event has + ever been performed to this device.""" from ereuse_devicehub.resources.device import states with suppress(LookupError, ValueError): event = self.last_event_of(*states.Trading.events()) @@ -126,7 +127,7 @@ class Device(Thing): @property def physical(self): - """The actual physical state, None if there is no state.""" + """The actual physical state, None otherwise.""" from ereuse_devicehub.resources.device import states with suppress(LookupError, ValueError): event = self.last_event_of(*states.Physical.events()) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 857ecbd9..db3f6a9a 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -31,11 +31,14 @@ class Device(Thing): 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) + lots = NestedOn('Lot', + many=True, + dump_only=True, + description='The lots where this device is directly under.') + rate = NestedOn('AggregateRate', dump_only=True, description=m.Device.rate.__doc__) + price = NestedOn('Price', dump_only=True, description=m.Device.price.__doc__) + trading = EnumField(states.Trading, dump_only=True, description=m.Device.trading.__doc__) + physical = EnumField(states.Physical, dump_only=True, description=m.Device.physical.__doc__) physical_possessor = NestedOn('Agent', dump_only=True, data_key='physicalPossessor') @pre_load diff --git a/ereuse_devicehub/resources/event/__init__.py b/ereuse_devicehub/resources/event/__init__.py index efc7fd45..23c0f21f 100644 --- a/ereuse_devicehub/resources/event/__init__.py +++ b/ereuse_devicehub/resources/event/__init__.py @@ -64,16 +64,6 @@ class WorkbenchRateDef(RateDef): SCHEMA = schemas.WorkbenchRate -class PhotoboxUserDef(RateDef): - VIEW = None - SCHEMA = schemas.PhotoboxUserRate - - -class PhotoboxSystemRateDef(RateDef): - VIEW = None - SCHEMA = schemas.PhotoboxSystemRate - - class ManualRateDef(RateDef): VIEW = None SCHEMA = schemas.ManualRate diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 758c39ea..4f7f49f1 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -356,8 +356,11 @@ class SnapshotRequest(db.Model): class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice): rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE)) + rating.comment = """The rating for the content.""" software = Column(DBEnum(RatingSoftware)) + software.comment = """The algorithm used to produce this rating.""" version = Column(StrictVersionType) + version.comment = """The version of the software.""" appearance = Column(Float(decimal_return_scale=2), check_range('appearance', *RATE_NEGATIVE)) functionality = Column(Float(decimal_return_scale=2), check_range('functionality', *RATE_NEGATIVE)) @@ -392,13 +395,9 @@ class ManualRate(IndividualRate): be removed. """ appearance_range = Column(DBEnum(AppearanceRange)) - appearance_range.comment = """Grades the imperfections that - aesthetically affect the device, but not its usage. - """ + appearance_range.comment = AppearanceRange.__doc__ functionality_range = Column(DBEnum(FunctionalityRange)) - functionality_range.comment = """Grades the defects of a device - affecting usage. - """ + functionality_range.comment = FunctionalityRange.__doc__ class WorkbenchRate(ManualRate): @@ -412,9 +411,7 @@ class WorkbenchRate(ManualRate): bios = Column(Float(decimal_return_scale=2), check_range('bios', *RATE_POSITIVE)) bios_range = Column(DBEnum(Bios)) - bios_range.comment = """How difficult it has been to set the bios - to boot from the network. - """ + bios_range.comment = Bios.__doc__ # todo ensure for WorkbenchRate version and software are not None when inserting them @@ -431,6 +428,12 @@ class WorkbenchRate(ManualRate): class AggregateRate(Rate): id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True) manual_id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id)) + manual_id.comment = """The ManualEvent used to generate this + aggregation, or None if none used. + + An example of ManualEvent is using the web or the Android app + to rate a device. + """ manual = relationship(ManualRate, backref=backref('aggregate_rate_manual', lazy=True, @@ -438,6 +441,9 @@ class AggregateRate(Rate): collection_class=OrderedSet), primaryjoin=manual_id == ManualRate.id) workbench_id = Column(UUID(as_uuid=True), ForeignKey(WorkbenchRate.id)) + workbench_id.comment = """The WorkbenchRate used to generate + this aggregation, or None if none used. + """ workbench = relationship(WorkbenchRate, backref=backref('aggregate_rate_workbench', lazy=True, @@ -483,10 +489,19 @@ class AggregateRate(Rate): class Price(JoinedWithOneDeviceMixin, EventWithOneDevice): currency = Column(DBEnum(Currency), nullable=False) + currency.comment = """The currency of this price as for ISO 4217.""" price = Column(Numeric(precision=19, scale=4), check_range('price', 0), nullable=False) + price.comment = """The value.""" software = Column(DBEnum(PriceSoftware)) + software.comment = """The software used to compute this price, + if the price was computed automatically. This field is None + if the price has been manually set. + """ version = Column(StrictVersionType) + version.comment = """The version of the software, or None.""" rating_id = Column(UUID(as_uuid=True), ForeignKey(AggregateRate.id)) + rating_id.comment = """The AggregateRate used to auto-compute + this price, if it has not been set manually.""" rating = relationship(AggregateRate, backref=backref('price', lazy=True, diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index b11d5e6e..5ef88664 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -146,6 +146,8 @@ class Rate(EventWithOneDevice): rating = ... # type: Column appearance = ... # type: Column functionality = ... # type: Column + software = ... # type: Column + version = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 4007b32d..1ba41030 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -102,12 +102,12 @@ class Rate(EventWithOneDevice): rating = Integer(validate=Range(*RATE_POSITIVE), dump_only=True, data_key='rating', - description='The rating for the content.') + description=m.Rate.rating.comment) software = EnumField(RatingSoftware, dump_only=True, - description='The algorithm used to produce this rating.') + description=m.Rate.software.comment) version = Version(dump_only=True, - description='The version of the software.') + description=m.Rate.version.comment) appearance = Integer(validate=Range(-3, 5), dump_only=True) functionality = Integer(validate=Range(-3, 5), dump_only=True, @@ -118,26 +118,6 @@ class IndividualRate(Rate): pass -class PhotoboxRate(IndividualRate): - num = Integer(dump_only=True) - # todo Image - - -class PhotoboxUserRate(IndividualRate): - assembling = Integer() - parts = Integer() - buttons = Integer() - dents = Integer() - decolorization = Integer() - scratches = Integer() - tag_adhesive = Integer() - dirt = Integer() - - -class PhotoboxSystemRate(IndividualRate): - pass - - class ManualRate(IndividualRate): appearance_range = EnumField(AppearanceRange, required=True, @@ -162,8 +142,11 @@ class WorkbenchRate(ManualRate): class AggregateRate(Rate): - workbench = NestedOn(WorkbenchRate, dump_only=True) - manual = NestedOn(ManualRate, dump_only=True) + workbench = NestedOn(WorkbenchRate, dump_only=True, + description=m.AggregateRate.workbench_id.comment) + manual = NestedOn(ManualRate, + dump_only=True, + description=m.AggregateRate.manual_id.comment) processor = Float(dump_only=True) ram = Float(dump_only=True) data_storage = Float(dump_only=True) @@ -172,11 +155,14 @@ class AggregateRate(Rate): class Price(EventWithOneDevice): - currency = EnumField(Currency, required=True) - price = Decimal(places=4, rounding=decimal.ROUND_HALF_EVEN, required=True) - software = EnumField(PriceSoftware, dump_only=True) - version = Version(dump_only=True) - rating = NestedOn(AggregateRate, dump_only=True) + currency = EnumField(Currency, required=True, description=m.Price.currency.comment) + price = Decimal(places=4, + ounding=decimal.ROUND_HALF_EVEN, + required=True, + description=m.Price.price.comment) + software = EnumField(PriceSoftware, dump_only=True, description=m.Price.software.comment) + version = Version(dump_only=True, description=m.Price.version.comment) + rating = NestedOn(AggregateRate, dump_only=True, description=m.Price.rating_id.comment) class EreusePrice(Price): diff --git a/tests/test_basic.py b/tests/test_basic.py index c1e7ee28..a1690145 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -39,4 +39,4 @@ def test_api_docs(client: Client): 'scheme': 'basic', 'name': 'Authorization' } - assert 77 == len(docs['definitions']) + assert 75 == len(docs['definitions']) From 2aaa79e8b4f134e8ed8ad181751cb9877fb71d9a Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sun, 14 Oct 2018 22:31:32 +0200 Subject: [PATCH 4/8] Fix one tag id per organization --- ereuse_devicehub/resources/tag/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 465e073d..1ef2a530 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -75,8 +75,8 @@ class Tag(Thing): return url __table_args__ = ( - UniqueConstraint(device_id, org_id, name='one_tag_per_org'), - UniqueConstraint(secondary, org_id, name='one_secondary_per_org') + UniqueConstraint(id, org_id, name='one tag id per organization'), + UniqueConstraint(secondary, org_id, name='one secondary tag per organization') ) def __repr__(self) -> str: From 97536c5ce12cc44be56b890b03cdaab5f508aec2 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Sun, 14 Oct 2018 23:56:54 +0200 Subject: [PATCH 5/8] Fix bug when device rate is low --- ereuse_devicehub/resources/event/models.py | 12 +- ereuse_devicehub/resources/event/models.pyi | 2 +- tests/files/asus-1001pxd.snapshot.yaml | 134 ++++++++++++++++++++ tests/test_workbench.py | 6 + 4 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 tests/files/asus-1001pxd.snapshot.yaml diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 4f7f49f1..ea6604bf 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -563,8 +563,9 @@ class EreusePrice(Price): def __init__(self, device, rating_range, role, price) -> None: cls = device.__class__ if device.__class__ != Server else Desktop rate = self.SCHEMA[cls][rating_range] - self.standard = EreusePrice.Type(rate['STD'][role], price) - self.warranty2 = EreusePrice.Type(rate['WR2'][role], price) + self.standard = EreusePrice.Type(rate[self.STANDARD][role], price) + if self.WARRANTY2 in rate: + self.warranty2 = EreusePrice.Type(rate[self.WARRANTY2][role], price) def __init__(self, rating: AggregateRate, **kwargs) -> None: if rating.rating_range == RatingRange.VERY_LOW: @@ -584,9 +585,10 @@ class EreusePrice(Price): self.refurbisher = self._service(self.Service.REFURBISHER) self.retailer = self._service(self.Service.RETAILER) self.platform = self._service(self.Service.PLATFORM) - self.warranty2 = round(self.refurbisher.warranty2.amount - + self.retailer.warranty2.amount - + self.platform.warranty2.amount, 2) + if hasattr(self.refurbisher, 'warranty2'): + self.warranty2 = round(self.refurbisher.warranty2.amount + + self.retailer.warranty2.amount + + self.platform.warranty2.amount, 2) def _service(self, role): return self.Service(self.device, self.rating.rating_range, role, self.price) diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index 5ef88664..a146c908 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -262,7 +262,7 @@ class EreusePrice(Price): MULTIPLIER = ... # type: Dict class Type: - def __init__(self) -> None: + def __init__(self, percentage, price) -> None: super().__init__() self.amount = ... # type: float self.percentage = ... # type: float diff --git a/tests/files/asus-1001pxd.snapshot.yaml b/tests/files/asus-1001pxd.snapshot.yaml new file mode 100644 index 00000000..2eaf0f43 --- /dev/null +++ b/tests/files/asus-1001pxd.snapshot.yaml @@ -0,0 +1,134 @@ +{ + "closed": true, + "components": [ + { + "events": [], + "manufacturer": "Intel Corporation", + "model": "NM10/ICH7 Family High Definition Audio Controller", + "serialNumber": null, + "type": "SoundCard" + }, + { + "events": [], + "manufacturer": "Azurewave", + "model": "USB 2.0 UVC VGA WebCam", + "serialNumber": "0x0001", + "type": "SoundCard" + }, + { + "events": [], + "format": "DIMM", + "interface": "DDR2", + "manufacturer": null, + "model": null, + "serialNumber": null, + "size": 1024, + "speed": 667.0, + "type": "RamModule" + }, + { + "address": 64, + "cores": 1, + "events": [ + { + "elapsed": 165, + "rate": 164.8342, + "type": "BenchmarkProcessorSysbench" + }, + { + "elapsed": 0, + "rate": 6665.7, + "type": "BenchmarkProcessor" + } + ], + "manufacturer": "Intel Corp.", + "model": "Intel Atom CPU N455 @ 1.66GHz", + "serialNumber": null, + "speed": 1.667, + "threads": 2, + "type": "Processor" + }, + { + "events": [ + { + "elapsed": 16, + "readSpeed": 66.2, + "type": "BenchmarkDataStorage", + "writeSpeed": 21.8 + } + ], + "interface": "ATA", + "manufacturer": "Hitachi", + "model": "HTS54322", + "serialNumber": "E2024242CV86HJ", + "size": 238475, + "type": "HardDrive" + }, + { + "events": [], + "manufacturer": "Qualcomm Atheros", + "model": "AR9285 Wireless Network Adapter", + "serialNumber": "74:2f:68:8b:fd:c8", + "type": "NetworkAdapter", + "wireless": true + }, + { + "events": [], + "manufacturer": "Qualcomm Atheros", + "model": "AR8152 v2.0 Fast Ethernet", + "serialNumber": "14:da:e9:42:f6:7c", + "speed": 100, + "type": "NetworkAdapter", + "wireless": false + }, + { + "events": [], + "manufacturer": "Intel Corporation", + "memory": 256.0, + "model": "Atom Processor D4xx/D5xx/N4xx/N5xx Integrated Graphics Controller", + "serialNumber": null, + "type": "GraphicCard" + }, + { + "events": [], + "firewire": 0, + "manufacturer": "ASUSTeK Computer INC.", + "model": "1001PXD", + "pcmcia": 0, + "serial": 1, + "serialNumber": "Eee0123456789", + "slots": 2, + "type": "Motherboard", + "usb": 5 + } + ], + "device": { + "chassis": "Netbook", + "events": [ + { + "elapsed": 16, + "rate": 15.8978, + "type": "BenchmarkRamSysbench" + }, + { + "appearanceRange": "A", + "biosRange": "A", + "functionalityRange": "A", + "type": "WorkbenchRate" + } + ], + "manufacturer": "ASUSTeK Computer INC.", + "model": "1001PXD", + "serialNumber": "B8OAAS048286", + "type": "Laptop" + }, + "elapsed": 6, + "endTime": "2018-10-14T21:22:14.777235+00:00", + "expectedEvents": [ + "Benchmark" + ], + "software": "Workbench", + "type": "Snapshot", + "uuid": "7dc4d19c-914e-4652-a381-d641325fb9c2", + "version": "11.0a6" +} diff --git a/tests/test_workbench.py b/tests/test_workbench.py index a2a3c8ef..0e8efc74 100644 --- a/tests/test_workbench.py +++ b/tests/test_workbench.py @@ -294,3 +294,9 @@ def test_workbench_fixtures(file: pathlib.Path, user: UserClient): user.post(res=em.Snapshot, data=s, status=201 if file.name not in SNAPSHOTS_NEED_ID else NeedsId) + + +def test_workbench_asus_1001pxd_rate_low(user: UserClient): + """Tests an Asus 1001pxd with a low rate.""" + s = file('asus-1001pxd.snapshot') + snapshot, _ = user.post(res=em.Snapshot, data=s) From 8889496a34543589b9427a5f2ef45d92625a7b1a Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Mon, 15 Oct 2018 11:21:21 +0200 Subject: [PATCH 6/8] Fix Price not always using Decimal --- ereuse_devicehub/resources/event/models.py | 37 +++++++++++++++------ ereuse_devicehub/resources/event/models.pyi | 6 ++++ ereuse_devicehub/resources/event/schemas.py | 6 ++-- tests/test_event.py | 12 ++++++- tests/test_rate.py | 17 +++++----- 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index ea6604bf..4c3e218c 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 decimal import Decimal, ROUND_HALF_EVEN, ROUND_UP from distutils.version import StrictVersion from typing import Set, Union from uuid import uuid4 @@ -488,9 +489,11 @@ class AggregateRate(Rate): class Price(JoinedWithOneDeviceMixin, EventWithOneDevice): + SCALE = 4 + ROUND = ROUND_HALF_EVEN currency = Column(DBEnum(Currency), nullable=False) currency.comment = """The currency of this price as for ISO 4217.""" - price = Column(Numeric(precision=19, scale=4), check_range('price', 0), nullable=False) + price = Column(Numeric(precision=19, scale=SCALE), check_range('price', 0), nullable=False) price.comment = """The value.""" software = Column(DBEnum(PriceSoftware)) software.comment = """The software used to compute this price, @@ -510,8 +513,17 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice): primaryjoin=AggregateRate.id == rating_id) def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - self.currency = self.currency or app.config['PRICE_CURRENCY'] + if 'price' in kwargs: + assert isinstance(kwargs['price'], Decimal), 'Price must be a Decimal' + super().__init__(currency=kwargs.pop('currency', app.config['PRICE_CURRENCY']), **kwargs) + + @classmethod + def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal: + """Returns a Decimal value with the correct scale for Price.price.""" + if isinstance(value, float): + value = Decimal(value) + # equation from marshmallow.fields.Decimal + return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding) class EreusePrice(Price): @@ -522,9 +534,10 @@ class EreusePrice(Price): } class Type: - def __init__(self, percentage, price) -> None: + def __init__(self, percentage: float, price: Decimal) -> None: # see https://stackoverflow.com/a/29651462 for the - 0.005 - self.amount = round(price * percentage - 0.005, 2) + self.amount = EreusePrice.to_price(price * Decimal(percentage)) + self.percentage = EreusePrice.to_price(price * Decimal(percentage)) self.percentage = round(percentage - 0.005, 2) class Service: @@ -560,7 +573,7 @@ class EreusePrice(Price): } SCHEMA[Server] = SCHEMA[Desktop] - def __init__(self, device, rating_range, role, price) -> None: + def __init__(self, device, rating_range, role, price: Decimal) -> None: cls = device.__class__ if device.__class__ != Server else Desktop rate = self.SCHEMA[cls][rating_range] self.standard = EreusePrice.Type(rate[self.STANDARD][role], price) @@ -570,11 +583,15 @@ class EreusePrice(Price): def __init__(self, rating: AggregateRate, **kwargs) -> None: if rating.rating_range == RatingRange.VERY_LOW: raise ValueError('Cannot compute price for Range.VERY_LOW') - self.price = round(rating.rating * self.MULTIPLIER[rating.device.__class__], 2) - super().__init__(rating=rating, device=rating.device, **kwargs) + # We pass ROUND_UP strategy so price is always greater than what refurbisher... amounts + price = self.to_price(rating.rating * self.MULTIPLIER[rating.device.__class__], ROUND_UP) + super().__init__(rating=rating, + device=rating.device, + price=price, + software=kwargs.pop('software', app.config['PRICE_SOFTWARE']), + version=kwargs.pop('version', app.config['PRICE_VERSION']), + **kwargs) self._compute() - self.software = self.software or app.config['PRICE_SOFTWARE'] - self.version = self.version or app.config['PRICE_VERSION'] @orm.reconstructor def _compute(self): diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index a146c908..fbac19e4 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -242,6 +242,8 @@ class WorkbenchRate(ManualRate): class Price(EventWithOneDevice): + SCALE = ... + ROUND = ... currency = ... # type: Column price = ... # type: Column software = ... # type: Column @@ -257,6 +259,10 @@ class Price(EventWithOneDevice): self.version = ... # type: StrictVersion self.rating = ... # type: AggregateRate + @classmethod + def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal: + pass + class EreusePrice(Price): MULTIPLIER = ... # type: Dict diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 1ba41030..242c8886 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -1,5 +1,3 @@ -import decimal - from flask import current_app as app from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \ @@ -156,8 +154,8 @@ class AggregateRate(Rate): class Price(EventWithOneDevice): currency = EnumField(Currency, required=True, description=m.Price.currency.comment) - price = Decimal(places=4, - ounding=decimal.ROUND_HALF_EVEN, + price = Decimal(places=m.Price.SCALE, + rounding=m.Price.ROUND, required=True, description=m.Price.price.comment) software = EnumField(PriceSoftware, dump_only=True, description=m.Price.software.comment) diff --git a/tests/test_event.py b/tests/test_event.py index 342e0356..cd4f150d 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -1,5 +1,6 @@ import ipaddress from datetime import timedelta +from decimal import Decimal from typing import Tuple import pytest @@ -265,7 +266,7 @@ def test_migrate(): def test_price_custom(): computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1', chassis=ComputerChassis.Docking) - price = models.Price(price=25.25, currency=Currency.EUR) + price = models.Price(price=Decimal(25.25), currency=Currency.EUR) price.device = computer assert computer.price == price db.session.add(computer) @@ -280,3 +281,12 @@ def test_price_custom(): c, _ = client.get(res=Device, item=computer.id) assert c['price']['id'] == p['id'] + + +@pytest.mark.xfail(reson='Develop test') +def test_ereuse_price(): + """Tests the several ways of creating eReuse Price, emulating + from an AggregateRate and ensuring that the different Range + return correct results.""" + # important to check Range.low no returning warranty2 + # Range.verylow not returning nothing diff --git a/tests/test_rate.py b/tests/test_rate.py index bf1cee90..99a9420b 100644 --- a/tests/test_rate.py +++ b/tests/test_rate.py @@ -1,3 +1,4 @@ +from decimal import Decimal from distutils.version import StrictVersion import pytest @@ -64,17 +65,17 @@ def test_rate(): 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 == Decimal('92.2001') + assert price.retailer.standard.amount == Decimal('40.9714') + assert price.platform.standard.amount == Decimal('18.8434') + assert price.refurbisher.standard.amount == Decimal('32.3853') 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 + assert price.retailer.warranty2.amount == Decimal('55.3085') + assert price.platform.warranty2.amount == Decimal('25.4357') + assert price.refurbisher.warranty2.amount == Decimal('43.7259') + assert price.warranty2 == Decimal('124.47') # 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)) From 5301242dbd445e9371fe2cf48012d6317c893189 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Mon, 15 Oct 2018 13:12:13 +0200 Subject: [PATCH 7/8] Fix not adding tag.secondary to search --- ereuse_devicehub/resources/device/search.py | 1 + tests/test_tag.py | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ereuse_devicehub/resources/device/search.py b/ereuse_devicehub/resources/device/search.py index 3970c7c8..8d330bfc 100644 --- a/ereuse_devicehub/resources/device/search.py +++ b/ereuse_devicehub/resources/device/search.py @@ -108,6 +108,7 @@ class DeviceSearch(db.Model): tags = session.query( search.Search.vectorize( (db.func.string_agg(Tag.id, ' '), search.Weight.A), + (db.func.string_agg(Tag.secondary, ' '), search.Weight.A), (db.func.string_agg(Organization.name, ' '), search.Weight.B) ) ).filter(Tag.device_id == device.id).join(Tag.org) diff --git a/tests/test_tag.py b/tests/test_tag.py index e0044d18..e20c2a7c 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -12,10 +12,12 @@ from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.agent.models import Organization from ereuse_devicehub.resources.device.models import Desktop, Device from ereuse_devicehub.resources.enums import ComputerChassis +from ereuse_devicehub.resources.event.models import Snapshot from ereuse_devicehub.resources.tag import Tag from ereuse_devicehub.resources.tag.view import CannotCreateETag, LinkedToAnotherDevice, \ TagNotLinked from tests import conftest +from tests.conftest import file @pytest.mark.usefixtures(conftest.app_context.__name__) @@ -179,8 +181,10 @@ def test_tag_manual_link(app: Devicehub, user: UserClient): @pytest.mark.usefixtures(conftest.app_context.__name__) -def test_tag_secondary(): - """Creates and consumes tags with a secondary id.""" +def test_tag_secondary_workbench_link_find(user: UserClient): + """Creates and consumes tags with a secondary id, linking them + through Workbench to a device + and getting them through search.""" t = Tag('foo', secondary='bar') db.session.add(t) db.session.flush() @@ -189,6 +193,18 @@ def test_tag_secondary(): with pytest.raises(ResourceNotFound): Tag.from_an_id('nope').one() + s = file('basic.snapshot') + s['device']['tags'] = [{'id': 'foo', 'secondary': 'bar', 'type': 'Tag'}] + snapshot, _ = user.post(s, res=Snapshot) + device, _ = user.get(res=Device, item=snapshot['device']['id']) + assert device['tags'][0]['id'] == 'foo' + assert device['tags'][0]['secondary'] == 'bar' + + r, _ = user.get(res=Device, query=[('search', 'foo'), ('filter', {'type': ['Computer']})]) + assert len(r['items']) == 1 + r, _ = user.get(res=Device, query=[('search', 'bar'), ('filter', {'type': ['Computer']})]) + assert len(r['items']) == 1 + def test_tag_create_tags_cli_csv(app: Devicehub, user: UserClient): """Checks creating tags with the CLI endpoint using a CSV.""" From 47f5bf69ae595652293a9ac7dfe49dd649fdd736 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Mon, 15 Oct 2018 14:20:26 +0200 Subject: [PATCH 8/8] Return ranges in AggregateRate --- ereuse_devicehub/resources/event/models.py | 18 ++++++++++++++++++ ereuse_devicehub/resources/event/models.pyi | 16 ++++++++++++++++ ereuse_devicehub/resources/event/schemas.py | 17 +++++++++++++---- tests/test_workbench.py | 2 ++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 4c3e218c..d8e53243 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -456,6 +456,8 @@ class AggregateRate(Rate): kwargs.setdefault('version', StrictVersion('1.0')) super().__init__(*args, **kwargs) + # todo take value from LAST event (manual or workbench) + @property def processor(self): return self.workbench.processor @@ -476,6 +478,22 @@ class AggregateRate(Rate): def bios(self): return self.workbench.bios + @property + def functionality_range(self): + return self.workbench.functionality_range + + @property + def appearance_range(self): + return self.workbench.appearance_range + + @property + def bios_range(self): + return self.workbench.bios_range + + @property + def labelling(self): + return self.workbench.labelling + @classmethod def from_workbench_rate(cls, rate: WorkbenchRate): aggregate = cls() diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index fbac19e4..e6ea8377 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -199,6 +199,22 @@ class AggregateRate(Rate): def bios(self): return self.workbench.bios + @property + def functionality_range(self): + return self.workbench.functionality_range + + @property + def appearance_range(self): + return self.workbench.appearance_range + + @property + def bios_range(self): + return self.workbench.bios_range + + @property + def labelling(self): + return self.workbench.labelling + @classmethod def from_workbench_rate(cls, rate: WorkbenchRate) -> AggregateRate: pass diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 242c8886..56732683 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -99,7 +99,6 @@ class StepRandom(Step): class Rate(EventWithOneDevice): rating = Integer(validate=Range(*RATE_POSITIVE), dump_only=True, - data_key='rating', description=m.Rate.rating.comment) software = EnumField(RatingSoftware, dump_only=True, @@ -107,9 +106,7 @@ class Rate(EventWithOneDevice): version = Version(dump_only=True, description=m.Rate.version.comment) appearance = Integer(validate=Range(-3, 5), dump_only=True) - functionality = Integer(validate=Range(-3, 5), - dump_only=True, - data_key='functionalityScore') + functionality = Integer(validate=Range(-3, 5), dump_only=True) class IndividualRate(Rate): @@ -150,6 +147,18 @@ class AggregateRate(Rate): data_storage = Float(dump_only=True) graphic_card = Float(dump_only=True) bios = EnumField(Bios, dump_only=True) + bios_range = EnumField(Bios, + description=m.WorkbenchRate.bios_range.comment, + data_key='biosRange') + appearance_range = EnumField(AppearanceRange, + required=True, + data_key='appearanceRange', + description=m.ManualRate.appearance_range.comment) + functionality_range = EnumField(FunctionalityRange, + required=True, + data_key='functionalityRange', + description=m.ManualRate.functionality_range.comment) + labelling = Boolean(description=m.ManualRate.labelling.comment) class Price(EventWithOneDevice): diff --git a/tests/test_workbench.py b/tests/test_workbench.py index 0e8efc74..f835ce21 100644 --- a/tests/test_workbench.py +++ b/tests/test_workbench.py @@ -62,6 +62,8 @@ def test_workbench_server_condensed(user: UserClient): assert not device['rate']['error'] assert device['rate']['rating'] == 0 assert device['rate']['workbench'] + assert device['rate']['appearanceRange'] == 'A' + assert device['rate']['functionalityRange'] == 'B' assert device['tags'][0]['id'] == 'tag1'