Merge remote-tracking branch 'origin/master' into reports
This commit is contained in:
commit
79f41f3501
|
@ -12,6 +12,7 @@ state Attributes {
|
||||||
state Usufructuarees
|
state Usufructuarees
|
||||||
state Reservees
|
state Reservees
|
||||||
state "Physical\nPossessor"
|
state "Physical\nPossessor"
|
||||||
|
state "Waste\n\Product"
|
||||||
}
|
}
|
||||||
|
|
||||||
state Physical {
|
state Physical {
|
||||||
|
@ -35,8 +36,17 @@ state Trading {
|
||||||
Reserved --> Cancelled : Cancel
|
Reserved --> Cancelled : Cancel
|
||||||
Sold --> Cancelled : Cancel
|
Sold --> Cancelled : Cancel
|
||||||
Sold --> Payed : Pay
|
Sold --> Payed : Pay
|
||||||
Registered --> ToBeDisposed
|
Registered --> ToBeDisposed : ToDisposeProduct
|
||||||
ToBeDisposed --> Disposed : DisposeProduct
|
ToBeDisposed --> ProductDisposed : DisposeProduct
|
||||||
|
Registered --> Donated: Donate
|
||||||
|
Registered --> Renting: Rent
|
||||||
|
Donated --> Cancelled : Cancel
|
||||||
|
Renting --> Cancelled : Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
state DataStoragePrivacyCompliance {
|
||||||
|
state Erased
|
||||||
|
state Destroyed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ device:
|
||||||
resolutionHeight: 1080
|
resolutionHeight: 1080
|
||||||
size: 21.5
|
size: 21.5
|
||||||
events:
|
events:
|
||||||
- type: AppRate
|
- type: ManualRate
|
||||||
appearanceRange: A
|
appearanceRange: A
|
||||||
functionalityRange: C
|
functionalityRange: C
|
||||||
labelling: False
|
labelling: False
|
||||||
|
|
|
@ -8,7 +8,7 @@ device:
|
||||||
serialNumber: ABCDEF
|
serialNumber: ABCDEF
|
||||||
imei: 35686800-004141-20
|
imei: 35686800-004141-20
|
||||||
events:
|
events:
|
||||||
- type: AppRate
|
- type: ManualRate
|
||||||
appearanceRange: A
|
appearanceRange: A
|
||||||
functionalityRange: B
|
functionalityRange: B
|
||||||
labelling: False
|
labelling: False
|
||||||
|
|
|
@ -26,6 +26,7 @@ device:
|
||||||
functionalityRange: B
|
functionalityRange: B
|
||||||
- type: BenchmarkRamSysbench
|
- type: BenchmarkRamSysbench
|
||||||
rate: 2444
|
rate: 2444
|
||||||
|
elapsed: 1
|
||||||
components:
|
components:
|
||||||
- type: GraphicCard
|
- type: GraphicCard
|
||||||
serialNumber: gc1-1s
|
serialNumber: gc1-1s
|
||||||
|
@ -35,22 +36,27 @@ components:
|
||||||
serialNumber: rm1-1s
|
serialNumber: rm1-1s
|
||||||
model: rm1-1ml
|
model: rm1-1ml
|
||||||
manufacturer: rm1-1mr
|
manufacturer: rm1-1mr
|
||||||
|
size: 1024
|
||||||
- type: RamModule
|
- type: RamModule
|
||||||
serialNumber: rm2-1s
|
serialNumber: rm2-1s
|
||||||
model: rm2-1ml
|
model: rm2-1ml
|
||||||
manufacturer: rm2-1mr
|
manufacturer: rm2-1mr
|
||||||
|
size: 1024
|
||||||
- type: Processor
|
- type: Processor
|
||||||
model: p1-1s
|
model: p1-1ml
|
||||||
manufacturer: p1-1mr
|
manufacturer: p1-1mr
|
||||||
events:
|
events:
|
||||||
- type: BenchmarkProcessor
|
- type: BenchmarkProcessor
|
||||||
rate: 2410
|
rate: 2410
|
||||||
|
elapsed: 44
|
||||||
- type: BenchmarkProcessorSysbench
|
- type: BenchmarkProcessorSysbench
|
||||||
rate: 4400
|
rate: 4400
|
||||||
|
elapsed: 44
|
||||||
- type: SolidStateDrive
|
- type: SolidStateDrive
|
||||||
serialNumber: ssd1-1s
|
serialNumber: ssd1-1s
|
||||||
model: ssd1-1ml
|
model: ssd1-1ml
|
||||||
manufacturer: ssd1-1mr
|
manufacturer: ssd1-1mr
|
||||||
|
size: 1100
|
||||||
events:
|
events:
|
||||||
- type: BenchmarkDataStorage
|
- type: BenchmarkDataStorage
|
||||||
readSpeed: 20
|
readSpeed: 20
|
||||||
|
@ -78,7 +84,24 @@ components:
|
||||||
- type: BenchmarkDataStorage
|
- type: BenchmarkDataStorage
|
||||||
readSpeed: 10
|
readSpeed: 10
|
||||||
writeSpeed: 5
|
writeSpeed: 5
|
||||||
|
elapsed: 20
|
||||||
- type: Motherboard
|
- type: Motherboard
|
||||||
serialNumber: mb1-1s
|
serialNumber: mb1-1s
|
||||||
model: mb1-1ml
|
model: mb1-1ml
|
||||||
manufacturer: mb1-1mr
|
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
|
||||||
|
|
|
@ -21,8 +21,8 @@ from teal.marshmallow import ValidationError
|
||||||
from teal.resource import url_for_resource
|
from teal.resource import url_for_resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
|
||||||
RamFormat, RamInterface
|
DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface
|
||||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,19 +44,22 @@ class Device(Thing):
|
||||||
model = Column(Unicode(), check_lower('model'))
|
model = Column(Unicode(), check_lower('model'))
|
||||||
manufacturer = Column(Unicode(), check_lower('manufacturer'))
|
manufacturer = Column(Unicode(), check_lower('manufacturer'))
|
||||||
serial_number = Column(Unicode(), check_lower('serial_number'))
|
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 = """
|
weight.comment = """
|
||||||
The weight of the device in Kgm.
|
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 = """
|
width.comment = """
|
||||||
The width of the device in meters.
|
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 = """
|
height.comment = """
|
||||||
The height of the device in meters.
|
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 = Column(ColorType)
|
||||||
color.comment = """The predominant color of the device."""
|
color.comment = """The predominant color of the device."""
|
||||||
|
|
||||||
|
@ -98,6 +101,55 @@ class Device(Thing):
|
||||||
"""The URL where to GET this device."""
|
"""The URL where to GET this device."""
|
||||||
return urlutils.URL(url_for_resource(Device, item_id=self.id))
|
return urlutils.URL(url_for_resource(Device, item_id=self.id))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rate(self):
|
||||||
|
"""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):
|
||||||
|
"""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 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())
|
||||||
|
return states.Trading(event.__class__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def physical(self):
|
||||||
|
"""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())
|
||||||
|
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
|
@declared_attr
|
||||||
def __mapper_args__(cls):
|
def __mapper_args__(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -112,6 +164,16 @@ class Device(Thing):
|
||||||
args[POLYMORPHIC_ON] = cls.type
|
args[POLYMORPHIC_ON] = cls.type
|
||||||
return args
|
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):
|
def __lt__(self, other):
|
||||||
return self.id < other.id
|
return self.id < other.id
|
||||||
|
|
||||||
|
@ -169,31 +231,38 @@ class Computer(Device):
|
||||||
@property
|
@property
|
||||||
def ram_size(self) -> int:
|
def ram_size(self) -> int:
|
||||||
"""The total of RAM memory the computer has."""
|
"""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
|
@property
|
||||||
def data_storage_size(self) -> int:
|
def data_storage_size(self) -> int:
|
||||||
"""The total of data storage the computer has."""
|
"""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
|
@property
|
||||||
def processor_model(self) -> str:
|
def processor_model(self) -> str:
|
||||||
"""The model of one of the processors of the computer."""
|
"""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
|
@property
|
||||||
def graphic_card_model(self) -> str:
|
def graphic_card_model(self) -> str:
|
||||||
"""The model of one of the graphic cards of the computer."""
|
"""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
|
@property
|
||||||
def network_speeds(self) -> List[int]:
|
def network_speeds(self) -> List[int]:
|
||||||
"""Returns two speeds: the first for the eth and the
|
"""Returns two values representing the speeds of the network
|
||||||
second for the wifi networks, or 0 respectively if not found.
|
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)):
|
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
|
return speeds
|
||||||
|
|
||||||
def __format__(self, format_spec):
|
def __format__(self, format_spec):
|
||||||
|
@ -322,6 +391,15 @@ class DataStorage(JoinedComponentTableMixin, Component):
|
||||||
"""
|
"""
|
||||||
interface = Column(DBEnum(DataStorageInterface))
|
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):
|
def __format__(self, format_spec):
|
||||||
v = super().__format__(format_spec)
|
v = super().__format__(format_spec)
|
||||||
if 's' in format_spec:
|
if 's' in format_spec:
|
||||||
|
@ -353,7 +431,7 @@ class NetworkMixin:
|
||||||
speed.comment = """
|
speed.comment = """
|
||||||
The maximum speed this network adapter can handle, in mbps.
|
The maximum speed this network adapter can handle, in mbps.
|
||||||
"""
|
"""
|
||||||
wireless = Column(Boolean)
|
wireless = Column(Boolean, nullable=False, default=False)
|
||||||
wireless.comment = """
|
wireless.comment = """
|
||||||
Whether it is a wireless interface.
|
Whether it is a wireless interface.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Dict, List, Set
|
from typing import Dict, List, Set, Type, Union
|
||||||
|
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from boltons.urlutils import URL
|
from boltons.urlutils import URL
|
||||||
|
@ -7,10 +7,11 @@ from sqlalchemy import Column, Integer
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from teal.db import Model
|
from teal.db import Model
|
||||||
|
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
from ereuse_devicehub.resources.agent.models import Agent
|
||||||
RamFormat, RamInterface
|
from ereuse_devicehub.resources.device import states
|
||||||
from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \
|
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
|
||||||
EventWithOneDevice
|
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.image.models import ImageList
|
||||||
from ereuse_devicehub.resources.lot.models import Lot
|
from ereuse_devicehub.resources.lot.models import Lot
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
@ -44,10 +45,10 @@ class Device(Thing):
|
||||||
self.height = ... # type: float
|
self.height = ... # type: float
|
||||||
self.depth = ... # type: float
|
self.depth = ... # type: float
|
||||||
self.color = ... # type: Color
|
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.physical_properties = ... # type: Dict[str, object or None]
|
||||||
self.events_multiple = ... # type: Set[EventWithMultipleDevices]
|
self.events_multiple = ... # type: Set[e.EventWithMultipleDevices]
|
||||||
self.events_one = ... # type: Set[EventWithOneDevice]
|
self.events_one = ... # type: Set[e.EventWithOneDevice]
|
||||||
self.images = ... # type: ImageList
|
self.images = ... # type: ImageList
|
||||||
self.tags = ... # type: Set[Tag]
|
self.tags = ... # type: Set[Tag]
|
||||||
self.lots = ... # type: Set[Lot]
|
self.lots = ... # type: Set[Lot]
|
||||||
|
@ -56,6 +57,30 @@ class Device(Thing):
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
pass
|
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:
|
class DisplayMixin:
|
||||||
technology = ... # type: Column
|
technology = ... # type: Column
|
||||||
size = ... # type: Column
|
size = ... # type: Column
|
||||||
|
@ -77,7 +102,7 @@ class Computer(DisplayMixin, Device):
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.components = ... # type: Set[Component]
|
self.components = ... # type: Set[Component]
|
||||||
self.events_parent = ... # type: Set[Event]
|
self.events_parent = ... # type: Set[e.Event]
|
||||||
self.chassis = ... # type: ComputerChassis
|
self.chassis = ... # type: ComputerChassis
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -104,6 +129,7 @@ class Computer(DisplayMixin, Device):
|
||||||
def network_speeds(self) -> List[int]:
|
def network_speeds(self) -> List[int]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Desktop(Computer):
|
class Desktop(Computer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -155,7 +181,7 @@ class Component(Device):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.parent_id = ... # type: int
|
self.parent_id = ... # type: int
|
||||||
self.parent = ... # type: Computer
|
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':
|
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
||||||
pass
|
pass
|
||||||
|
@ -178,6 +204,10 @@ class DataStorage(Component):
|
||||||
self.size = ... # type: int
|
self.size = ... # type: int
|
||||||
self.interface = ... # type: DataStorageInterface
|
self.interface = ... # type: DataStorageInterface
|
||||||
|
|
||||||
|
@property
|
||||||
|
def privacy(self) -> DataStoragePrivacyCompliance:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HardDrive(DataStorage):
|
class HardDrive(DataStorage):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from marshmallow import post_load, pre_load
|
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 marshmallow.validate import Length, OneOf, Range
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
from stdnum import imei, meid
|
from stdnum import imei, meid
|
||||||
|
@ -7,9 +7,9 @@ from teal.marshmallow import EnumField, SanitizedStr, URL, ValidationError
|
||||||
from teal.resource import Schema
|
from teal.resource import Schema
|
||||||
|
|
||||||
from ereuse_devicehub.marshmallow import NestedOn
|
from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from ereuse_devicehub.resources.device import models as m
|
from ereuse_devicehub.resources.device import models as m, states
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
|
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
|
||||||
RamFormat, RamInterface
|
DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface
|
||||||
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
|
||||||
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
|
||||||
|
|
||||||
|
@ -24,13 +24,22 @@ class Device(Thing):
|
||||||
model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE))
|
model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE))
|
||||||
manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE))
|
manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE))
|
||||||
serial_number = SanitizedStr(lower=True, data_key='serialNumber')
|
serial_number = SanitizedStr(lower=True, data_key='serialNumber')
|
||||||
weight = Float(validate=Range(0.1, 3), unit=UnitCodes.kgm, description=m.Device.weight.comment)
|
weight = Float(validate=Range(0.1, 5), unit=UnitCodes.kgm, description=m.Device.weight.comment)
|
||||||
width = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.width.comment)
|
width = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.width.comment)
|
||||||
height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.height.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 = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__)
|
||||||
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
|
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
|
||||||
url = URL(dump_only=True, description=m.Device.url.__doc__)
|
url = URL(dump_only=True, description=m.Device.url.__doc__)
|
||||||
lots = NestedOn('Lot', many=True, 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
|
@pre_load
|
||||||
def from_events_to_events_one(self, data: dict):
|
def from_events_to_events_one(self, data: dict):
|
||||||
|
@ -60,6 +69,11 @@ class Device(Thing):
|
||||||
class Computer(Device):
|
class Computer(Device):
|
||||||
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
|
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
|
||||||
chassis = EnumField(ComputerChassis, required=True)
|
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):
|
class Desktop(Computer):
|
||||||
|
@ -148,6 +162,7 @@ class DataStorage(Component):
|
||||||
unit=UnitCodes.mbyte,
|
unit=UnitCodes.mbyte,
|
||||||
description=m.DataStorage.size.comment)
|
description=m.DataStorage.size.comment)
|
||||||
interface = EnumField(DataStorageInterface)
|
interface = EnumField(DataStorageInterface)
|
||||||
|
privacy = EnumField(DataStoragePrivacyCompliance, dump_only=True)
|
||||||
|
|
||||||
|
|
||||||
class HardDrive(DataStorage):
|
class HardDrive(DataStorage):
|
||||||
|
|
|
@ -108,6 +108,7 @@ class DeviceSearch(db.Model):
|
||||||
tags = session.query(
|
tags = session.query(
|
||||||
search.Search.vectorize(
|
search.Search.vectorize(
|
||||||
(db.func.string_agg(Tag.id, ' '), search.Weight.A),
|
(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)
|
(db.func.string_agg(Organization.name, ' '), search.Weight.B)
|
||||||
)
|
)
|
||||||
).filter(Tag.device_id == device.id).join(Tag.org)
|
).filter(Tag.device_id == device.id).join(Tag.org)
|
||||||
|
|
|
@ -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
|
|
@ -235,3 +235,21 @@ class ReceiverRole(Enum):
|
||||||
CollectionPoint = 'A collection point.'
|
CollectionPoint = 'A collection point.'
|
||||||
RecyclingPoint = 'A recycling point.'
|
RecyclingPoint = 'A recycling point.'
|
||||||
Transporter = 'An user that ships the devices to another one.'
|
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
|
||||||
|
|
|
@ -64,19 +64,9 @@ class WorkbenchRateDef(RateDef):
|
||||||
SCHEMA = schemas.WorkbenchRate
|
SCHEMA = schemas.WorkbenchRate
|
||||||
|
|
||||||
|
|
||||||
class PhotoboxUserDef(RateDef):
|
class ManualRateDef(RateDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.PhotoboxUserRate
|
SCHEMA = schemas.ManualRate
|
||||||
|
|
||||||
|
|
||||||
class PhotoboxSystemRateDef(RateDef):
|
|
||||||
VIEW = None
|
|
||||||
SCHEMA = schemas.PhotoboxSystemRate
|
|
||||||
|
|
||||||
|
|
||||||
class AppRateDef(RateDef):
|
|
||||||
VIEW = None
|
|
||||||
SCHEMA = schemas.AppRate
|
|
||||||
|
|
||||||
|
|
||||||
class PriceDef(EventDef):
|
class PriceDef(EventDef):
|
||||||
|
@ -98,7 +88,8 @@ class SnapshotDef(EventDef):
|
||||||
VIEW = SnapshotView
|
VIEW = SnapshotView
|
||||||
SCHEMA = schemas.Snapshot
|
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,
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
|
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
|
||||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
from datetime import datetime, timedelta
|
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 typing import Set, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
@ -24,17 +26,12 @@ from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.agent.models import Agent
|
from ereuse_devicehub.resources.agent.models import Agent
|
||||||
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
|
||||||
Device, Laptop, Server
|
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, \
|
FunctionalityRange, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
|
||||||
ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
|
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.models import STR_SM_SIZE, Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
"""
|
|
||||||
A quantity of money with a currency.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class JoinedTableMixin:
|
class JoinedTableMixin:
|
||||||
# noinspection PyMethodParameters
|
# noinspection PyMethodParameters
|
||||||
|
@ -54,7 +51,7 @@ class Event(Thing):
|
||||||
incidence.comment = """
|
incidence.comment = """
|
||||||
Should this event be reviewed due some anomaly?
|
Should this event be reviewed due some anomaly?
|
||||||
"""
|
"""
|
||||||
closed = Column(Boolean, default=False, nullable=False)
|
closed = Column(Boolean, default=True, nullable=False)
|
||||||
closed.comment = """
|
closed.comment = """
|
||||||
Whether the author has finished the event.
|
Whether the author has finished the event.
|
||||||
After this is set to True, no modifications are allowed.
|
After this is set to True, no modifications are allowed.
|
||||||
|
@ -360,8 +357,11 @@ class SnapshotRequest(db.Model):
|
||||||
|
|
||||||
class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
|
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
|
||||||
|
rating.comment = """The rating for the content."""
|
||||||
software = Column(DBEnum(RatingSoftware))
|
software = Column(DBEnum(RatingSoftware))
|
||||||
|
software.comment = """The algorithm used to produce this rating."""
|
||||||
version = Column(StrictVersionType)
|
version = Column(StrictVersionType)
|
||||||
|
version.comment = """The version of the software."""
|
||||||
appearance = Column(Float(decimal_return_scale=2), check_range('appearance', *RATE_NEGATIVE))
|
appearance = Column(Float(decimal_return_scale=2), check_range('appearance', *RATE_NEGATIVE))
|
||||||
functionality = Column(Float(decimal_return_scale=2),
|
functionality = Column(Float(decimal_return_scale=2),
|
||||||
check_range('functionality', *RATE_NEGATIVE))
|
check_range('functionality', *RATE_NEGATIVE))
|
||||||
|
@ -389,35 +389,16 @@ class IndividualRate(Rate):
|
||||||
pass
|
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):
|
class ManualRate(IndividualRate):
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
||||||
labelling = Column(Boolean)
|
labelling = Column(Boolean)
|
||||||
|
labelling.comment = """Sets if there are labels stuck that should
|
||||||
|
be removed.
|
||||||
|
"""
|
||||||
appearance_range = Column(DBEnum(AppearanceRange))
|
appearance_range = Column(DBEnum(AppearanceRange))
|
||||||
|
appearance_range.comment = AppearanceRange.__doc__
|
||||||
functionality_range = Column(DBEnum(FunctionalityRange))
|
functionality_range = Column(DBEnum(FunctionalityRange))
|
||||||
|
functionality_range.comment = FunctionalityRange.__doc__
|
||||||
|
|
||||||
|
|
||||||
class WorkbenchRate(ManualRate):
|
class WorkbenchRate(ManualRate):
|
||||||
|
@ -428,63 +409,120 @@ class WorkbenchRate(ManualRate):
|
||||||
check_range('data_storage', *RATE_POSITIVE))
|
check_range('data_storage', *RATE_POSITIVE))
|
||||||
graphic_card = Column(Float(decimal_return_scale=2),
|
graphic_card = Column(Float(decimal_return_scale=2),
|
||||||
check_range('graphic_card', *RATE_POSITIVE))
|
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 = Bios.__doc__
|
||||||
|
|
||||||
# todo ensure for WorkbenchRate version and software are not None when inserting them
|
# 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.
|
Computes all the possible rates taking this rating as a model.
|
||||||
|
|
||||||
Returns a set of ratings, including this one, which is mutated.
|
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_'))
|
return main(self, **app.config.get_namespace('WORKBENCH_RATE_'))
|
||||||
|
|
||||||
|
|
||||||
class AppRate(ManualRate):
|
class AggregateRate(Rate):
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PhotoboxRate(IndividualRate):
|
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
|
||||||
image_id = Column(UUID(as_uuid=True), ForeignKey(Image.id), nullable=False)
|
manual_id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id))
|
||||||
image = relationship(Image,
|
manual_id.comment = """The ManualEvent used to generate this
|
||||||
uselist=False,
|
aggregation, or None if none used.
|
||||||
cascade=CASCADE_OWN,
|
|
||||||
single_parent=True,
|
|
||||||
primaryjoin=Image.id == image_id)
|
|
||||||
|
|
||||||
# todo how to ensure phtoboxrate.device == image.image_list.device?
|
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,
|
||||||
|
order_by=lambda: AggregateRate.created,
|
||||||
|
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,
|
||||||
|
order_by=lambda: AggregateRate.created,
|
||||||
|
collection_class=OrderedSet),
|
||||||
|
primaryjoin=workbench_id == WorkbenchRate.id)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
kwargs.setdefault('version', StrictVersion('1.0'))
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
class PhotoboxUserRate(PhotoboxRate):
|
# todo take value from LAST event (manual or workbench)
|
||||||
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 processor(self):
|
||||||
|
return self.workbench.processor
|
||||||
|
|
||||||
class PhotoboxSystemRate(PhotoboxRate):
|
@property
|
||||||
id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True)
|
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
|
||||||
|
|
||||||
|
@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()
|
||||||
|
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):
|
class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
|
SCALE = 4
|
||||||
|
ROUND = ROUND_HALF_EVEN
|
||||||
currency = Column(DBEnum(Currency), nullable=False)
|
currency = Column(DBEnum(Currency), nullable=False)
|
||||||
price = Column(Numeric(precision=19, scale=4), check_range('price', 0), nullable=False)
|
currency.comment = """The currency of this price as for ISO 4217."""
|
||||||
|
price = Column(Numeric(precision=19, scale=SCALE), check_range('price', 0), nullable=False)
|
||||||
|
price.comment = """The value."""
|
||||||
software = Column(DBEnum(PriceSoftware))
|
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 = Column(StrictVersionType)
|
||||||
|
version.comment = """The version of the software, or None."""
|
||||||
rating_id = Column(UUID(as_uuid=True), ForeignKey(AggregateRate.id))
|
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,
|
rating = relationship(AggregateRate,
|
||||||
backref=backref('price',
|
backref=backref('price',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
|
@ -493,8 +531,17 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
|
||||||
primaryjoin=AggregateRate.id == rating_id)
|
primaryjoin=AggregateRate.id == rating_id)
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
if 'price' in kwargs:
|
||||||
self.currency = self.currency or app.config['PRICE_CURRENCY']
|
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):
|
class EreusePrice(Price):
|
||||||
|
@ -505,9 +552,10 @@ class EreusePrice(Price):
|
||||||
}
|
}
|
||||||
|
|
||||||
class Type:
|
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
|
# 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)
|
self.percentage = round(percentage - 0.005, 2)
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
|
@ -543,20 +591,25 @@ class EreusePrice(Price):
|
||||||
}
|
}
|
||||||
SCHEMA[Server] = SCHEMA[Desktop]
|
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
|
cls = device.__class__ if device.__class__ != Server else Desktop
|
||||||
rate = self.SCHEMA[cls][rating_range]
|
rate = self.SCHEMA[cls][rating_range]
|
||||||
self.standard = EreusePrice.Type(rate['STD'][role], price)
|
self.standard = EreusePrice.Type(rate[self.STANDARD][role], price)
|
||||||
self.warranty2 = EreusePrice.Type(rate['WR2'][role], price)
|
if self.WARRANTY2 in rate:
|
||||||
|
self.warranty2 = EreusePrice.Type(rate[self.WARRANTY2][role], price)
|
||||||
|
|
||||||
def __init__(self, rating: AggregateRate, **kwargs) -> None:
|
def __init__(self, rating: AggregateRate, **kwargs) -> None:
|
||||||
if rating.rating_range == RatingRange.VERY_LOW:
|
if rating.rating_range == RatingRange.VERY_LOW:
|
||||||
raise ValueError('Cannot compute price for Range.VERY_LOW')
|
raise ValueError('Cannot compute price for Range.VERY_LOW')
|
||||||
self.price = round(rating.rating * self.MULTIPLIER[rating.device.__class__], 2)
|
# We pass ROUND_UP strategy so price is always greater than what refurbisher... amounts
|
||||||
super().__init__(rating=rating, device=rating.device, **kwargs)
|
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._compute()
|
||||||
self.software = self.software or app.config['PRICE_SOFTWARE']
|
|
||||||
self.version = self.version or app.config['PRICE_VERSION']
|
|
||||||
|
|
||||||
@orm.reconstructor
|
@orm.reconstructor
|
||||||
def _compute(self):
|
def _compute(self):
|
||||||
|
@ -567,9 +620,10 @@ class EreusePrice(Price):
|
||||||
self.refurbisher = self._service(self.Service.REFURBISHER)
|
self.refurbisher = self._service(self.Service.REFURBISHER)
|
||||||
self.retailer = self._service(self.Service.RETAILER)
|
self.retailer = self._service(self.Service.RETAILER)
|
||||||
self.platform = self._service(self.Service.PLATFORM)
|
self.platform = self._service(self.Service.PLATFORM)
|
||||||
self.warranty2 = round(self.refurbisher.warranty2.amount
|
if hasattr(self.refurbisher, 'warranty2'):
|
||||||
+ self.retailer.warranty2.amount
|
self.warranty2 = round(self.refurbisher.warranty2.amount
|
||||||
+ self.platform.warranty2.amount, 2)
|
+ self.retailer.warranty2.amount
|
||||||
|
+ self.platform.warranty2.amount, 2)
|
||||||
|
|
||||||
def _service(self, role):
|
def _service(self, role):
|
||||||
return self.Service(self.device, self.rating.rating_range, role, self.price)
|
return self.Service(self.device, self.rating.rating_range, role, self.price)
|
||||||
|
|
|
@ -19,7 +19,6 @@ from ereuse_devicehub.resources.device.models import Component, Computer, Device
|
||||||
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
|
||||||
PriceSoftware, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, \
|
PriceSoftware, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, \
|
||||||
TestDataStorageLength
|
TestDataStorageLength
|
||||||
from ereuse_devicehub.resources.image.models import Image
|
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
|
|
||||||
|
@ -78,7 +77,7 @@ class EventWithOneDevice(Event):
|
||||||
|
|
||||||
|
|
||||||
class EventWithMultipleDevices(Event):
|
class EventWithMultipleDevices(Event):
|
||||||
devices = ... # type: relationship
|
devices = ... # type: relationship
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
|
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,
|
description=None, start_time=None, end_time=None, snapshot=None, agent=None,
|
||||||
|
@ -147,6 +146,8 @@ class Rate(EventWithOneDevice):
|
||||||
rating = ... # type: Column
|
rating = ... # type: Column
|
||||||
appearance = ... # type: Column
|
appearance = ... # type: Column
|
||||||
functionality = ... # type: Column
|
functionality = ... # type: Column
|
||||||
|
software = ... # type: Column
|
||||||
|
version = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
@ -165,59 +166,100 @@ class IndividualRate(Rate):
|
||||||
|
|
||||||
|
|
||||||
class AggregateRate(Rate):
|
class AggregateRate(Rate):
|
||||||
|
manual_id = ... # type: Column
|
||||||
|
manual = ... # type: relationship
|
||||||
|
workbench = ... # type: relationship
|
||||||
|
workbench_id = ... # type: Column
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
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
|
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
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
|
||||||
class ManualRate(IndividualRate):
|
class ManualRate(IndividualRate):
|
||||||
|
labelling = ... # type: Column
|
||||||
|
appearance_range = ... # type: Column
|
||||||
|
functionality_range = ... # type: Column
|
||||||
|
aggregate_rate_manual = ... #type: relationship
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.labelling = ... # type: bool
|
self.labelling = ... # type: bool
|
||||||
self.appearance_range = ... # type: AppearanceRange
|
self.appearance_range = ... # type: AppearanceRange
|
||||||
self.functionality_range = ... # type: FunctionalityRange
|
self.functionality_range = ... # type: FunctionalityRange
|
||||||
|
self.aggregate_rate_manual = ... #type: AggregateRate
|
||||||
|
|
||||||
|
|
||||||
class WorkbenchRate(ManualRate):
|
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:
|
def __init__(self, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.processor = ... # type: float
|
self.processor = ... # type: float
|
||||||
self.ram = ... # type: float
|
self.ram = ... # type: float
|
||||||
self.data_storage = ... # type: float
|
self.data_storage = ... # type: float
|
||||||
self.graphic_card = ... # 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]:
|
||||||
class AppRate(ManualRate):
|
pass
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Price(EventWithOneDevice):
|
class Price(EventWithOneDevice):
|
||||||
|
SCALE = ...
|
||||||
|
ROUND = ...
|
||||||
currency = ... # type: Column
|
currency = ... # type: Column
|
||||||
price = ... # type: Column
|
price = ... # type: Column
|
||||||
software = ... # type: Column
|
software = ... # type: Column
|
||||||
|
@ -233,12 +275,32 @@ class Price(EventWithOneDevice):
|
||||||
self.version = ... # type: StrictVersion
|
self.version = ... # type: StrictVersion
|
||||||
self.rating = ... # type: AggregateRate
|
self.rating = ... # type: AggregateRate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EreusePrice(Price):
|
class EreusePrice(Price):
|
||||||
MULTIPLIER = ... # type: Dict
|
MULTIPLIER = ... # type: Dict
|
||||||
|
|
||||||
|
class Type:
|
||||||
|
def __init__(self, percentage, price) -> 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:
|
def __init__(self, rating: AggregateRate, **kwargs) -> None:
|
||||||
super().__init__(**kwargs)
|
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):
|
class Test(EventWithOneDevice):
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -1,5 +1,3 @@
|
||||||
import decimal
|
|
||||||
|
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
|
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
|
||||||
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
|
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
|
||||||
|
@ -101,62 +99,30 @@ class StepRandom(Step):
|
||||||
class Rate(EventWithOneDevice):
|
class Rate(EventWithOneDevice):
|
||||||
rating = Integer(validate=Range(*RATE_POSITIVE),
|
rating = Integer(validate=Range(*RATE_POSITIVE),
|
||||||
dump_only=True,
|
dump_only=True,
|
||||||
data_key='ratingValue',
|
description=m.Rate.rating.comment)
|
||||||
description='The rating for the content.')
|
|
||||||
software = EnumField(RatingSoftware,
|
software = EnumField(RatingSoftware,
|
||||||
dump_only=True,
|
dump_only=True,
|
||||||
description='The algorithm used to produce this rating.')
|
description=m.Rate.software.comment)
|
||||||
version = Version(dump_only=True,
|
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)
|
appearance = Integer(validate=Range(-3, 5), dump_only=True)
|
||||||
functionality = Integer(validate=Range(-3, 5),
|
functionality = Integer(validate=Range(-3, 5), dump_only=True)
|
||||||
dump_only=True,
|
|
||||||
data_key='functionalityScore')
|
|
||||||
|
|
||||||
|
|
||||||
class IndividualRate(Rate):
|
class IndividualRate(Rate):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AggregateRate(Rate):
|
|
||||||
ratings = NestedOn(IndividualRate, many=True)
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
class ManualRate(IndividualRate):
|
||||||
appearance_range = EnumField(AppearanceRange,
|
appearance_range = EnumField(AppearanceRange,
|
||||||
required=True,
|
required=True,
|
||||||
data_key='appearanceRange',
|
data_key='appearanceRange',
|
||||||
description='Grades the imperfections that aesthetically '
|
description=m.ManualRate.appearance_range.comment)
|
||||||
'affect the device, but not its usage.')
|
|
||||||
functionality_range = EnumField(FunctionalityRange,
|
functionality_range = EnumField(FunctionalityRange,
|
||||||
required=True,
|
required=True,
|
||||||
data_key='functionalityRange',
|
data_key='functionalityRange',
|
||||||
description='Grades the defects of a device affecting usage.')
|
description=m.ManualRate.functionality_range.comment)
|
||||||
labelling = Boolean(description='Sets if there are labels stuck that should be removed.')
|
labelling = Boolean(description=m.ManualRate.labelling.comment)
|
||||||
|
|
||||||
|
|
||||||
class AppRate(ManualRate):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WorkbenchRate(ManualRate):
|
class WorkbenchRate(ManualRate):
|
||||||
|
@ -164,16 +130,46 @@ class WorkbenchRate(ManualRate):
|
||||||
ram = Float()
|
ram = Float()
|
||||||
data_storage = Float()
|
data_storage = Float()
|
||||||
graphic_card = Float()
|
graphic_card = Float()
|
||||||
bios = EnumField(Bios, description='How difficult it has been to set the bios to '
|
bios = Float()
|
||||||
'boot from the network.')
|
bios_range = EnumField(Bios,
|
||||||
|
description=m.WorkbenchRate.bios_range.comment,
|
||||||
|
data_key='biosRange')
|
||||||
|
|
||||||
|
|
||||||
|
class AggregateRate(Rate):
|
||||||
|
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)
|
||||||
|
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):
|
class Price(EventWithOneDevice):
|
||||||
currency = EnumField(Currency, required=True)
|
currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
|
||||||
price = Decimal(places=4, rounding=decimal.ROUND_HALF_EVEN, required=True)
|
price = Decimal(places=m.Price.SCALE,
|
||||||
software = EnumField(PriceSoftware, dump_only=True)
|
rounding=m.Price.ROUND,
|
||||||
version = Version(dump_only=True)
|
required=True,
|
||||||
rating = NestedOn(AggregateRate, dump_only=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):
|
class EreusePrice(Price):
|
||||||
|
@ -285,7 +281,7 @@ class StressTest(Test):
|
||||||
|
|
||||||
|
|
||||||
class Benchmark(EventWithOneDevice):
|
class Benchmark(EventWithOneDevice):
|
||||||
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
|
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkDataStorage(Benchmark):
|
class BenchmarkDataStorage(Benchmark):
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from contextlib import suppress
|
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from typing import List
|
from typing import List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
@ -77,11 +76,9 @@ class SnapshotView(View):
|
||||||
snapshot.events |= events
|
snapshot.events |= events
|
||||||
|
|
||||||
# Compute ratings
|
# Compute ratings
|
||||||
with suppress(StopIteration):
|
for rate in (e for e in events_device if isinstance(e, WorkbenchRate)):
|
||||||
# todo are we sure we want to have snapshots without rates?
|
rates = rate.ratings()
|
||||||
snapshot.events |= next(
|
snapshot.events |= rates
|
||||||
e.ratings() for e in events_device if isinstance(e, WorkbenchRate)
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(snapshot)
|
db.session.add(snapshot)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
@ -15,7 +15,8 @@ class LotDef(Resource):
|
||||||
AUTH = True
|
AUTH = True
|
||||||
ID_CONVERTER = Converters.uuid
|
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,
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
|
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
|
||||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
|
|
|
@ -75,8 +75,8 @@ class Tag(Thing):
|
||||||
return url
|
return url
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(device_id, org_id, name='one_tag_per_org'),
|
UniqueConstraint(id, org_id, name='one tag id per organization'),
|
||||||
UniqueConstraint(secondary, org_id, name='one_secondary_per_org')
|
UniqueConstraint(secondary, org_id, name='one secondary tag per organization')
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|
|
@ -5,7 +5,6 @@ click==6.7
|
||||||
click-spinner==0.1.8
|
click-spinner==0.1.8
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
colour==0.1.5
|
colour==0.1.5
|
||||||
ereuse-rate==0.0.2
|
|
||||||
ereuse-utils==0.4.0b9
|
ereuse-utils==0.4.0b9
|
||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
Flask-Cors==3.0.6
|
Flask-Cors==3.0.6
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -37,7 +37,6 @@ setup(
|
||||||
'teal>=0.2.0a24', # teal always first
|
'teal>=0.2.0a24', # teal always first
|
||||||
'click',
|
'click',
|
||||||
'click-spinner',
|
'click-spinner',
|
||||||
'ereuse-rate==0.0.2',
|
|
||||||
'ereuse-utils[Naming]>=0.4b9',
|
'ereuse-utils[Naming]>=0.4b9',
|
||||||
'hashids',
|
'hashids',
|
||||||
'marshmallow_enum',
|
'marshmallow_enum',
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ device:
|
||||||
appearanceRange: A
|
appearanceRange: A
|
||||||
functionalityRange: B
|
functionalityRange: B
|
||||||
labelling: True
|
labelling: True
|
||||||
bios: B
|
biosRange: B
|
||||||
components:
|
components:
|
||||||
- type: GraphicCard
|
- type: GraphicCard
|
||||||
serialNumber: gc1s
|
serialNumber: gc1s
|
||||||
|
@ -33,3 +33,4 @@ components:
|
||||||
events:
|
events:
|
||||||
- type: BenchmarkProcessor
|
- type: BenchmarkProcessor
|
||||||
rate: 2410
|
rate: 2410
|
||||||
|
elapsed: 11
|
||||||
|
|
|
@ -39,4 +39,4 @@ def test_api_docs(client: Client):
|
||||||
'scheme': 'basic',
|
'scheme': 'basic',
|
||||||
'name': 'Authorization'
|
'name': 'Authorization'
|
||||||
}
|
}
|
||||||
assert 77 == len(docs['definitions'])
|
assert 75 == len(docs['definitions'])
|
||||||
|
|
|
@ -91,7 +91,14 @@ def test_physical_properties():
|
||||||
manufacturer='mr',
|
manufacturer='mr',
|
||||||
width=2.0,
|
width=2.0,
|
||||||
color=Color())
|
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)
|
pc.components.add(c)
|
||||||
db.session.add(pc)
|
db.session.add(pc)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -110,6 +117,17 @@ def test_physical_properties():
|
||||||
'color': Color(),
|
'color': Color(),
|
||||||
'depth': None
|
'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__)
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import current_app as app, g
|
from flask import current_app as app, g
|
||||||
|
@ -8,6 +10,7 @@ from teal.enums import Currency, Subdivision
|
||||||
|
|
||||||
from ereuse_devicehub.client import UserClient
|
from ereuse_devicehub.client import UserClient
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.resources.device import states
|
||||||
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
|
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
|
||||||
RamModule, SolidStateDrive
|
RamModule, SolidStateDrive
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis, TestDataStorageLength
|
from ereuse_devicehub.resources.enums import ComputerChassis, TestDataStorageLength
|
||||||
|
@ -175,22 +178,24 @@ def test_update_parent():
|
||||||
assert not benchmark.parent
|
assert not benchmark.parent
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('event_model', [
|
@pytest.mark.parametrize('event_model_state', [
|
||||||
models.ToRepair,
|
(models.ToRepair, states.Physical.ToBeRepaired),
|
||||||
models.Repair,
|
(models.Repair, states.Physical.Repaired),
|
||||||
models.ToPrepare,
|
(models.ToPrepare, states.Physical.Preparing),
|
||||||
models.ReadyToUse,
|
(models.ReadyToUse, states.Physical.ReadyToBeUsed),
|
||||||
models.ToPrepare,
|
(models.ToPrepare, states.Physical.Preparing),
|
||||||
models.Prepare,
|
(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."""
|
"""Tests POSTing all generic events."""
|
||||||
|
event_model, state = event_model_state
|
||||||
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
|
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
|
||||||
event = {'type': event_model.t, 'devices': [snapshot['device']['id']]}
|
event = {'type': event_model.t, 'devices': [snapshot['device']['id']]}
|
||||||
event, _ = user.post(event, res=models.Event)
|
event, _ = user.post(event, res=models.Event)
|
||||||
assert event['devices'][0]['id'] == snapshot['device']['id']
|
assert event['devices'][0]['id'] == snapshot['device']['id']
|
||||||
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||||
assert device['events'][-1]['id'] == event['id']
|
assert device['events'][-1]['id'] == event['id']
|
||||||
|
assert device['physical'] == state.name
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
||||||
|
@ -214,6 +219,8 @@ def test_live():
|
||||||
assert live['ip'] == '79.147.10.10'
|
assert live['ip'] == '79.147.10.10'
|
||||||
assert live['subdivision'] == 'ES-CA'
|
assert live['subdivision'] == 'ES-CA'
|
||||||
assert live['country'] == 'ES'
|
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.')
|
@pytest.mark.xfail(reson='Functionality not developed.')
|
||||||
|
@ -226,14 +233,15 @@ def test_reserve(user: UserClient):
|
||||||
"""Performs a reservation and then cancels it."""
|
"""Performs a reservation and then cancels it."""
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('event_model', [
|
@pytest.mark.parametrize('event_model_state', [
|
||||||
models.Sell,
|
(models.Sell, states.Trading.Sold),
|
||||||
models.Donate,
|
(models.Donate, states.Trading.Donated),
|
||||||
models.Rent,
|
(models.Rent, states.Trading.Renting),
|
||||||
models.DisposeProduct
|
(models.DisposeProduct, states.Trading.ProductDisposed)
|
||||||
])
|
])
|
||||||
def test_trade(event_model: models.Event, user: UserClient):
|
def test_trade(event_model_state: Tuple[models.Event, states.Trading], user: UserClient):
|
||||||
"""Tests POSTing all generic events."""
|
"""Tests POSTing all Trade events."""
|
||||||
|
event_model, state = event_model_state
|
||||||
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
|
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
|
||||||
event = {
|
event = {
|
||||||
'type': event_model.t,
|
'type': event_model.t,
|
||||||
|
@ -246,6 +254,7 @@ def test_trade(event_model: models.Event, user: UserClient):
|
||||||
assert event['devices'][0]['id'] == snapshot['device']['id']
|
assert event['devices'][0]['id'] == snapshot['device']['id']
|
||||||
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
device, _ = user.get(res=Device, item=snapshot['device']['id'])
|
||||||
assert device['events'][-1]['id'] == event['id']
|
assert device['events'][-1]['id'] == event['id']
|
||||||
|
assert device['trading'] == state.name
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reson='Develop migrate')
|
@pytest.mark.xfail(reson='Develop migrate')
|
||||||
|
@ -257,8 +266,9 @@ def test_migrate():
|
||||||
def test_price_custom():
|
def test_price_custom():
|
||||||
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1',
|
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1',
|
||||||
chassis=ComputerChassis.Docking)
|
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
|
price.device = computer
|
||||||
|
assert computer.price == price
|
||||||
db.session.add(computer)
|
db.session.add(computer)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -268,3 +278,15 @@ def test_price_custom():
|
||||||
assert p['device']['id'] == price.device.id == computer.id
|
assert p['device']['id'] == price.device.id == computer.id
|
||||||
assert p['price'] == 25.25
|
assert p['price'] == 25.25
|
||||||
assert p['currency'] == Currency.EUR.name == 'EUR'
|
assert p['currency'] == Currency.EUR.name == 'EUR'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -254,7 +254,7 @@ def test_post_get_lot(user: UserClient):
|
||||||
assert not l['children']
|
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
|
"""Tests adding children lots to a lot through the view and
|
||||||
GETting the results."""
|
GETting the results."""
|
||||||
parent, _ = user.post(({'name': 'Parent'}), res=Lot)
|
parent, _ = user.post(({'name': 'Parent'}), res=Lot)
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
|
from decimal import Decimal
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.device.models import Computer, Desktop
|
from ereuse_devicehub.resources.device.models import Computer, Desktop, HardDrive, Processor, \
|
||||||
from ereuse_devicehub.resources.enums import Bios, ComputerChassis, ImageMimeTypes, Orientation, \
|
RamModule
|
||||||
RatingSoftware
|
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, ComputerChassis, \
|
||||||
from ereuse_devicehub.resources.event.models import PhotoboxRate, WorkbenchRate
|
FunctionalityRange, RatingSoftware
|
||||||
from ereuse_devicehub.resources.image.models import Image, ImageList
|
from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkDataStorage, \
|
||||||
|
BenchmarkProcessor, EreusePrice, WorkbenchRate
|
||||||
|
from ereuse_devicehub.resources.event.rate import main
|
||||||
from tests import conftest
|
from tests import conftest
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +18,7 @@ from tests import conftest
|
||||||
def test_workbench_rate_db():
|
def test_workbench_rate_db():
|
||||||
rate = WorkbenchRate(processor=0.1,
|
rate = WorkbenchRate(processor=0.1,
|
||||||
ram=1.0,
|
ram=1.0,
|
||||||
bios=Bios.A,
|
bios_range=Bios.A,
|
||||||
labelling=False,
|
labelling=False,
|
||||||
graphic_card=0.1,
|
graphic_card=0.1,
|
||||||
data_storage=4.1,
|
data_storage=4.1,
|
||||||
|
@ -26,17 +29,63 @@ def test_workbench_rate_db():
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
|
@pytest.mark.xfail(reason='AggreagteRate only takes data from WorkbenchRate as for now')
|
||||||
def test_photobox_rate_db():
|
def test_rate_workbench_then_manual():
|
||||||
pc = Desktop(serial_number='24', chassis=ComputerChassis.Tower)
|
"""Checks that a new AggregateRate is generated with a new rate
|
||||||
image = Image(name='foo',
|
value when a ManualRate is performed after performing a
|
||||||
content=b'123',
|
WorkbenchRate.
|
||||||
file_format=ImageMimeTypes.jpg,
|
|
||||||
orientation=Orientation.Horizontal,
|
The new AggregateRate needs to be computed by the values of
|
||||||
image_list=ImageList(device=pc))
|
the WorkbenchRate + new values from ManualRate.
|
||||||
rate = PhotoboxRate(image=image,
|
"""
|
||||||
software=RatingSoftware.ECost,
|
pass
|
||||||
version=StrictVersion('1.0'),
|
|
||||||
device=pc)
|
|
||||||
db.session.add(rate)
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
db.session.commit()
|
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 == 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 == 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))
|
||||||
|
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
|
||||||
|
|
|
@ -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."""
|
|
@ -1,9 +1,9 @@
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from distutils.version import StrictVersion
|
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from boltons import urlutils
|
||||||
from teal.db import UniqueViolation
|
from teal.db import UniqueViolation
|
||||||
from teal.marshmallow import ValidationError
|
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 import models as m
|
||||||
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
from ereuse_devicehub.resources.device.exceptions import NeedsId
|
||||||
from ereuse_devicehub.resources.device.sync import MismatchBetweenTagsAndHid
|
from ereuse_devicehub.resources.device.sync import MismatchBetweenTagsAndHid
|
||||||
from ereuse_devicehub.resources.enums import Bios, ComputerChassis, RatingSoftware, \
|
from ereuse_devicehub.resources.enums import ComputerChassis, SnapshotSoftware
|
||||||
SnapshotSoftware
|
|
||||||
from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkProcessor, \
|
from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkProcessor, \
|
||||||
EraseSectors, Event, Snapshot, SnapshotRequest, WorkbenchRate
|
EraseSectors, Event, Snapshot, SnapshotRequest, WorkbenchRate
|
||||||
from ereuse_devicehub.resources.tag import Tag
|
from ereuse_devicehub.resources.tag import Tag
|
||||||
|
@ -37,21 +36,11 @@ def test_snapshot_model():
|
||||||
elapsed=timedelta(seconds=25))
|
elapsed=timedelta(seconds=25))
|
||||||
snapshot.device = device
|
snapshot.device = device
|
||||||
snapshot.request = SnapshotRequest(request={'foo': 'bar'})
|
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.add(snapshot)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
device = m.Desktop.query.one() # type: m.Desktop
|
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(e1, Snapshot), 'Creation order must be preserved: 1. snapshot, 2. WR'
|
||||||
assert isinstance(e2, WorkbenchRate)
|
|
||||||
db.session.delete(device)
|
db.session.delete(device)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
assert Snapshot.query.one_or_none() is None
|
assert Snapshot.query.one_or_none() is None
|
||||||
|
@ -59,6 +48,8 @@ def test_snapshot_model():
|
||||||
assert User.query.one() is not None
|
assert User.query.one() is not None
|
||||||
assert m.Desktop.query.one_or_none() is None
|
assert m.Desktop.query.one_or_none() is None
|
||||||
assert m.Device.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):
|
def test_snapshot_schema(app: Devicehub):
|
||||||
|
@ -321,27 +312,37 @@ def test_erase(user: UserClient):
|
||||||
assert step['type'] == 'StepZero'
|
assert step['type'] == 'StepZero'
|
||||||
assert step['error'] is False
|
assert step['error'] is False
|
||||||
assert 'num' not in step
|
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):
|
def test_snapshot_computer_monitor(user: UserClient):
|
||||||
s = file('computer-monitor.snapshot')
|
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):
|
def test_snapshot_mobile_smartphone(user: UserClient):
|
||||||
s = file('smartphone.snapshot')
|
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():
|
def test_snapshot_components_none():
|
||||||
"""
|
"""
|
||||||
Tests that a snapshot without components does not
|
Tests that a snapshot without components does not
|
||||||
remove them from the computer.
|
remove them from the computer.
|
||||||
"""
|
"""
|
||||||
# todo test
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason='Test not developed')
|
||||||
def test_snapshot_components_empty():
|
def test_snapshot_components_empty():
|
||||||
"""
|
"""
|
||||||
Tests that a snapshot whose components are an empty list remove
|
Tests that a snapshot whose components are an empty list remove
|
||||||
|
|
|
@ -12,10 +12,12 @@ from ereuse_devicehub.devicehub import Devicehub
|
||||||
from ereuse_devicehub.resources.agent.models import Organization
|
from ereuse_devicehub.resources.agent.models import Organization
|
||||||
from ereuse_devicehub.resources.device.models import Desktop, Device
|
from ereuse_devicehub.resources.device.models import Desktop, Device
|
||||||
from ereuse_devicehub.resources.enums import ComputerChassis
|
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 import Tag
|
||||||
from ereuse_devicehub.resources.tag.view import CannotCreateETag, LinkedToAnotherDevice, \
|
from ereuse_devicehub.resources.tag.view import CannotCreateETag, LinkedToAnotherDevice, \
|
||||||
TagNotLinked
|
TagNotLinked
|
||||||
from tests import conftest
|
from tests import conftest
|
||||||
|
from tests.conftest import file
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
@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__)
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
def test_tag_secondary():
|
def test_tag_secondary_workbench_link_find(user: UserClient):
|
||||||
"""Creates and consumes tags with a secondary id."""
|
"""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')
|
t = Tag('foo', secondary='bar')
|
||||||
db.session.add(t)
|
db.session.add(t)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
@ -189,6 +193,18 @@ def test_tag_secondary():
|
||||||
with pytest.raises(ResourceNotFound):
|
with pytest.raises(ResourceNotFound):
|
||||||
Tag.from_an_id('nope').one()
|
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):
|
def test_tag_create_tags_cli_csv(app: Devicehub, user: UserClient):
|
||||||
"""Checks creating tags with the CLI endpoint using a CSV."""
|
"""Checks creating tags with the CLI endpoint using a CSV."""
|
||||||
|
|
|
@ -27,7 +27,7 @@ def test_workbench_server_condensed(user: UserClient):
|
||||||
file('workbench-server-3.erase'),
|
file('workbench-server-3.erase'),
|
||||||
file('workbench-server-4.install')
|
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
|
# Create tags
|
||||||
for t in s['device']['tags']:
|
for t in s['device']['tags']:
|
||||||
user.post({'id': t['id']}, res=Tag)
|
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)
|
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
||||||
events = snapshot['events']
|
events = snapshot['events']
|
||||||
assert {(event['type'], event['device']) for event in events} == {
|
assert {(event['type'], event['device']) for event in events} == {
|
||||||
# todo missing Rate event aggregating the rates
|
('AggregateRate', 1),
|
||||||
('WorkbenchRate', 1),
|
('WorkbenchRate', 1),
|
||||||
('BenchmarkProcessorSysbench', 5),
|
('BenchmarkProcessorSysbench', 5),
|
||||||
('StressTest', 1),
|
('StressTest', 1),
|
||||||
|
@ -45,10 +45,26 @@ def test_workbench_server_condensed(user: UserClient):
|
||||||
('Install', 6),
|
('Install', 6),
|
||||||
('EraseSectors', 7),
|
('EraseSectors', 7),
|
||||||
('BenchmarkDataStorage', 6),
|
('BenchmarkDataStorage', 6),
|
||||||
|
('BenchmarkDataStorage', 7),
|
||||||
('TestDataStorage', 6)
|
('TestDataStorage', 6)
|
||||||
}
|
}
|
||||||
assert snapshot['closed']
|
assert snapshot['closed']
|
||||||
assert not snapshot['error']
|
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['rate']['appearanceRange'] == 'A'
|
||||||
|
assert device['rate']['functionalityRange'] == 'B'
|
||||||
|
assert device['tags'][0]['id'] == 'tag1'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='Functionality not yet developed.')
|
@pytest.mark.xfail(reason='Functionality not yet developed.')
|
||||||
|
@ -122,8 +138,9 @@ def test_workbench_server_phases(user: UserClient):
|
||||||
def test_real_hp_11(user: UserClient):
|
def test_real_hp_11(user: UserClient):
|
||||||
s = file('real-hp.snapshot.11')
|
s = file('real-hp.snapshot.11')
|
||||||
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
||||||
assert snapshot['device']['hid'] == 'hewlett-packard-czc0408yjg-hp_compaq_8100_elite_sff'
|
pc = snapshot['device']
|
||||||
assert snapshot['device']['chassis'] == 'Tower'
|
assert pc['hid'] == 'hewlett-packard-czc0408yjg-hp_compaq_8100_elite_sff'
|
||||||
|
assert pc['chassis'] == 'Tower'
|
||||||
assert set(e['type'] for e in snapshot['events']) == {
|
assert set(e['type'] for e in snapshot['events']) == {
|
||||||
'BenchmarkDataStorage',
|
'BenchmarkDataStorage',
|
||||||
'BenchmarkProcessor',
|
'BenchmarkProcessor',
|
||||||
|
@ -133,6 +150,10 @@ def test_real_hp_11(user: UserClient):
|
||||||
'StressTest'
|
'StressTest'
|
||||||
}
|
}
|
||||||
assert len(list(e['type'] for e in snapshot['events'])) == 6
|
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):
|
def test_real_toshiba_11(user: UserClient):
|
||||||
|
@ -140,7 +161,7 @@ def test_real_toshiba_11(user: UserClient):
|
||||||
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
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,
|
Checks the values of the device, components,
|
||||||
events and their relationships of a real pc.
|
events and their relationships of a real pc.
|
||||||
|
@ -155,6 +176,7 @@ def test_real_eee_1001pxd(user: UserClient):
|
||||||
assert pc['manufacturer'] == 'asustek computer inc.'
|
assert pc['manufacturer'] == 'asustek computer inc.'
|
||||||
assert pc['hid'] == 'asustek_computer_inc-b8oaas048286-1001pxd'
|
assert pc['hid'] == 'asustek_computer_inc-b8oaas048286-1001pxd'
|
||||||
assert pc['tags'] == []
|
assert pc['tags'] == []
|
||||||
|
assert pc['networkSpeeds'] == [100, 0], 'Although it has WiFi we do not know the speed'
|
||||||
components = snapshot['components']
|
components = snapshot['components']
|
||||||
wifi = components[0]
|
wifi = components[0]
|
||||||
assert wifi['hid'] == 'qualcomm_atheros-74_2f_68_8b_fd_c8-ar9285_wireless_network_adapter'
|
assert wifi['hid'] == 'qualcomm_atheros-74_2f_68_8b_fd_c8-ar9285_wireless_network_adapter'
|
||||||
|
@ -170,7 +192,7 @@ def test_real_eee_1001pxd(user: UserClient):
|
||||||
assert cpu['threads'] == 1
|
assert cpu['threads'] == 1
|
||||||
assert cpu['speed'] == 1.667
|
assert cpu['speed'] == 1.667
|
||||||
assert 'hid' not in cpu
|
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'])
|
cpu, _ = user.get(res=Device, item=cpu['id'])
|
||||||
events = cpu['events']
|
events = cpu['events']
|
||||||
sysbench = next(e for e in events if e['type'] == em.BenchmarkProcessorSysbench.t)
|
sysbench = next(e for e in events if e['type'] == em.BenchmarkProcessorSysbench.t)
|
||||||
|
@ -204,6 +226,7 @@ def test_real_eee_1001pxd(user: UserClient):
|
||||||
ram = components[6]
|
ram = components[6]
|
||||||
assert ram['interface'] == 'DDR2'
|
assert ram['interface'] == 'DDR2'
|
||||||
assert ram['speed'] == 667
|
assert ram['speed'] == 667
|
||||||
|
assert pc['ramSize'] == ram['size'] == 1024
|
||||||
hdd = components[7]
|
hdd = components[7]
|
||||||
assert hdd['type'] == 'HardDrive'
|
assert hdd['type'] == 'HardDrive'
|
||||||
assert hdd['hid'] == 'hitachi-e2024242cv86hj-hts54322'
|
assert hdd['hid'] == 'hitachi-e2024242cv86hj-hts54322'
|
||||||
|
@ -223,6 +246,7 @@ def test_real_eee_1001pxd(user: UserClient):
|
||||||
assert erase['startTime']
|
assert erase['startTime']
|
||||||
assert erase['zeros'] is False
|
assert erase['zeros'] is False
|
||||||
assert erase['error'] is False
|
assert erase['error'] is False
|
||||||
|
assert hdd['privacy'] == 'EraseBasic'
|
||||||
mother = components[8]
|
mother = components[8]
|
||||||
assert mother['hid'] == 'asustek_computer_inc-eee0123456789-1001pxd'
|
assert mother['hid'] == 'asustek_computer_inc-eee0123456789-1001pxd'
|
||||||
|
|
||||||
|
@ -243,6 +267,11 @@ def test_real_eee_1000h(user: UserClient):
|
||||||
snapshot, _ = user.post(res=em.Snapshot, data=s)
|
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 = {
|
SNAPSHOTS_NEED_ID = {
|
||||||
'box-xavier.snapshot.json',
|
'box-xavier.snapshot.json',
|
||||||
'custom.lshw.snapshot.json',
|
'custom.lshw.snapshot.json',
|
||||||
|
@ -267,3 +296,9 @@ def test_workbench_fixtures(file: pathlib.Path, user: UserClient):
|
||||||
user.post(res=em.Snapshot,
|
user.post(res=em.Snapshot,
|
||||||
data=s,
|
data=s,
|
||||||
status=201 if file.name not in SNAPSHOTS_NEED_ID else NeedsId)
|
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)
|
||||||
|
|
Reference in New Issue