Merge remote-tracking branch 'origin/master' into reports

This commit is contained in:
JNadeu 2018-10-15 14:50:59 +02:00
commit 79f41f3501
35 changed files with 1668 additions and 286 deletions

View File

@ -12,6 +12,7 @@ state Attributes {
state Usufructuarees
state Reservees
state "Physical\nPossessor"
state "Waste\n\Product"
}
state Physical {
@ -35,8 +36,17 @@ state Trading {
Reserved --> Cancelled : Cancel
Sold --> Cancelled : Cancel
Sold --> Payed : Pay
Registered --> ToBeDisposed
ToBeDisposed --> Disposed : DisposeProduct
Registered --> ToBeDisposed : ToDisposeProduct
ToBeDisposed --> ProductDisposed : DisposeProduct
Registered --> Donated: Donate
Registered --> Renting: Rent
Donated --> Cancelled : Cancel
Renting --> Cancelled : Cancel
}
state DataStoragePrivacyCompliance {
state Erased
state Destroyed
}

View File

@ -11,7 +11,7 @@ device:
resolutionHeight: 1080
size: 21.5
events:
- type: AppRate
- type: ManualRate
appearanceRange: A
functionalityRange: C
labelling: False

View File

@ -8,7 +8,7 @@ device:
serialNumber: ABCDEF
imei: 35686800-004141-20
events:
- type: AppRate
- type: ManualRate
appearanceRange: A
functionalityRange: B
labelling: False

View File

@ -26,6 +26,7 @@ device:
functionalityRange: B
- type: BenchmarkRamSysbench
rate: 2444
elapsed: 1
components:
- type: GraphicCard
serialNumber: gc1-1s
@ -35,22 +36,27 @@ components:
serialNumber: rm1-1s
model: rm1-1ml
manufacturer: rm1-1mr
size: 1024
- type: RamModule
serialNumber: rm2-1s
model: rm2-1ml
manufacturer: rm2-1mr
size: 1024
- type: Processor
model: p1-1s
model: p1-1ml
manufacturer: p1-1mr
events:
- type: BenchmarkProcessor
rate: 2410
elapsed: 44
- type: BenchmarkProcessorSysbench
rate: 4400
elapsed: 44
- type: SolidStateDrive
serialNumber: ssd1-1s
model: ssd1-1ml
manufacturer: ssd1-1mr
size: 1100
events:
- type: BenchmarkDataStorage
readSpeed: 20
@ -78,7 +84,24 @@ components:
- type: BenchmarkDataStorage
readSpeed: 10
writeSpeed: 5
elapsed: 20
- type: Motherboard
serialNumber: mb1-1s
model: mb1-1ml
manufacturer: mb1-1mr
- type: NetworkAdapter
serialNumber: na1-s
model: na1-1ml
manufacturer: na1-1mr
speed: 1000
wireless: False
- type: NetworkAdapter
serialNumber: na2-s
model: na2-1ml
manufacturer: na2-1mr
wireless: True
speed: 58
- type: RamModule
serialNumber: rm3-1s
model: rm3-1ml
manufacturer: rm3-1mr

View File

@ -21,8 +21,8 @@ from teal.marshmallow import ValidationError
from teal.resource import url_for_resource
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
RamFormat, RamInterface
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
@ -44,19 +44,22 @@ class Device(Thing):
model = Column(Unicode(), check_lower('model'))
manufacturer = Column(Unicode(), check_lower('manufacturer'))
serial_number = Column(Unicode(), check_lower('serial_number'))
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 3))
weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5))
weight.comment = """
The weight of the device in Kgm.
"""
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 3))
width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5))
width.comment = """
The width of the device in meters.
"""
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 3))
height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5))
height.comment = """
The height of the device in meters.
"""
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 3))
depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5))
depth.comment = """
The depth of the device in meters.
"""
color = Column(ColorType)
color.comment = """The predominant color of the device."""
@ -98,6 +101,55 @@ class Device(Thing):
"""The URL where to GET this device."""
return urlutils.URL(url_for_resource(Device, item_id=self.id))
@property
def rate(self):
"""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
def __mapper_args__(cls):
"""
@ -112,6 +164,16 @@ class Device(Thing):
args[POLYMORPHIC_ON] = cls.type
return args
def last_event_of(self, *types):
"""Gets the last event of the given types.
:raise LookupError: Device has not an event of the given type.
"""
try:
return next(e for e in reversed(self.events) if isinstance(e, types))
except StopIteration:
raise LookupError('{!r} does not contain events of types {}.'.format(self, types))
def __lt__(self, other):
return self.id < other.id
@ -169,31 +231,38 @@ class Computer(Device):
@property
def ram_size(self) -> int:
"""The total of RAM memory the computer has."""
return sum(ram.size for ram in self.components if isinstance(ram, RamModule))
return sum(ram.size or 0 for ram in self.components if isinstance(ram, RamModule))
@property
def data_storage_size(self) -> int:
"""The total of data storage the computer has."""
return sum(ds.size for ds in self.components if isinstance(ds, DataStorage))
return sum(ds.size or 0 for ds in self.components if isinstance(ds, DataStorage))
@property
def processor_model(self) -> str:
"""The model of one of the processors of the computer."""
return next(p.model for p in self.components if isinstance(p, Processor))
return next((p.model for p in self.components if isinstance(p, Processor)), None)
@property
def graphic_card_model(self) -> str:
"""The model of one of the graphic cards of the computer."""
return next(p.model for p in self.components if isinstance(p, GraphicCard))
return next((p.model for p in self.components if isinstance(p, GraphicCard)), None)
@property
def network_speeds(self) -> List[int]:
"""Returns two speeds: the first for the eth and the
second for the wifi networks, or 0 respectively if not found.
"""Returns two values representing the speeds of the network
adapters of the device.
1. The max Ethernet speed of the computer, 0 if ethernet
adaptor exists but its speed is unknown, None if no eth
adaptor exists.
2. The max WiFi speed of the computer, 0 if computer has
WiFi but its speed is unknown, None if no WiFi adaptor
exists.
"""
speeds = [0, 0]
speeds = [None, None]
for net in (c for c in self.components if isinstance(c, NetworkAdapter)):
speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless])
speeds[net.wireless] = max(net.speed or 0, speeds[net.wireless] or 0)
return speeds
def __format__(self, format_spec):
@ -322,6 +391,15 @@ class DataStorage(JoinedComponentTableMixin, Component):
"""
interface = Column(DBEnum(DataStorageInterface))
@property
def privacy(self):
"""Returns the privacy compliance state of the data storage."""
# todo add physical destruction event
from ereuse_devicehub.resources.event.models import EraseBasic
with suppress(LookupError):
erase = self.last_event_of(EraseBasic)
return DataStoragePrivacyCompliance.from_erase(erase)
def __format__(self, format_spec):
v = super().__format__(format_spec)
if 's' in format_spec:
@ -353,7 +431,7 @@ class NetworkMixin:
speed.comment = """
The maximum speed this network adapter can handle, in mbps.
"""
wireless = Column(Boolean)
wireless = Column(Boolean, nullable=False, default=False)
wireless.comment = """
Whether it is a wireless interface.
"""

View File

@ -1,4 +1,4 @@
from typing import Dict, List, Set
from typing import Dict, List, Set, Type, Union
from boltons import urlutils
from boltons.urlutils import URL
@ -7,10 +7,11 @@ from sqlalchemy import Column, Integer
from sqlalchemy.orm import relationship
from teal.db import Model
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
RamFormat, RamInterface
from ereuse_devicehub.resources.event.models import Event, EventWithMultipleDevices, \
EventWithOneDevice
from ereuse_devicehub.resources.agent.models import Agent
from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface
from ereuse_devicehub.resources.event import models as e
from ereuse_devicehub.resources.image.models import ImageList
from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.models import Thing
@ -44,10 +45,10 @@ class Device(Thing):
self.height = ... # type: float
self.depth = ... # type: float
self.color = ... # type: Color
self.events = ... # type: List[Event]
self.events = ... # type: List[e.Event]
self.physical_properties = ... # type: Dict[str, object or None]
self.events_multiple = ... # type: Set[EventWithMultipleDevices]
self.events_one = ... # type: Set[EventWithOneDevice]
self.events_multiple = ... # type: Set[e.EventWithMultipleDevices]
self.events_one = ... # type: Set[e.EventWithOneDevice]
self.images = ... # type: ImageList
self.tags = ... # type: Set[Tag]
self.lots = ... # type: Set[Lot]
@ -56,6 +57,30 @@ class Device(Thing):
def url(self) -> urlutils.URL:
pass
@property
def rate(self) -> Union[e.AggregateRate, None]:
pass
@property
def price(self) -> Union[e.Price, None]:
pass
@property
def trading(self) -> Union[states.Trading, None]:
pass
@property
def physical(self) -> Union[states.Physical, None]:
pass
@property
def physical_possessor(self) -> Union[Agent, None]:
pass
def last_event_of(self, *types: Type[e.Event]) -> e.Event:
pass
class DisplayMixin:
technology = ... # type: Column
size = ... # type: Column
@ -77,7 +102,7 @@ class Computer(DisplayMixin, Device):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.components = ... # type: Set[Component]
self.events_parent = ... # type: Set[Event]
self.events_parent = ... # type: Set[e.Event]
self.chassis = ... # type: ComputerChassis
@property
@ -104,6 +129,7 @@ class Computer(DisplayMixin, Device):
def network_speeds(self) -> List[int]:
pass
class Desktop(Computer):
pass
@ -155,7 +181,7 @@ class Component(Device):
super().__init__(**kwargs)
self.parent_id = ... # type: int
self.parent = ... # type: Computer
self.events_components = ... # type: Set[Event]
self.events_components = ... # type: Set[e.Event]
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
pass
@ -178,6 +204,10 @@ class DataStorage(Component):
self.size = ... # type: int
self.interface = ... # type: DataStorageInterface
@property
def privacy(self) -> DataStoragePrivacyCompliance:
pass
class HardDrive(DataStorage):
pass

View File

@ -1,5 +1,5 @@
from marshmallow import post_load, pre_load
from marshmallow.fields import Boolean, Float, Integer, Str, String
from marshmallow.fields import Boolean, Float, Integer, List, Str, String
from marshmallow.validate import Length, OneOf, Range
from sqlalchemy.util import OrderedSet
from stdnum import imei, meid
@ -7,9 +7,9 @@ from teal.marshmallow import EnumField, SanitizedStr, URL, ValidationError
from teal.resource import Schema
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.device import models as m
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \
RamFormat, RamInterface
from ereuse_devicehub.resources.device import models as m, states
from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, \
DataStoragePrivacyCompliance, DisplayTech, RamFormat, RamInterface
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes
@ -24,13 +24,22 @@ class Device(Thing):
model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE))
manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE))
serial_number = SanitizedStr(lower=True, data_key='serialNumber')
weight = Float(validate=Range(0.1, 3), unit=UnitCodes.kgm, description=m.Device.weight.comment)
width = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.width.comment)
height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description=m.Device.height.comment)
weight = Float(validate=Range(0.1, 5), unit=UnitCodes.kgm, description=m.Device.weight.comment)
width = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.width.comment)
height = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.height.comment)
depth = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.depth.comment)
events = NestedOn('Event', many=True, dump_only=True, description=m.Device.events.__doc__)
events_one = NestedOn('Event', many=True, load_only=True, collection_class=OrderedSet)
url = URL(dump_only=True, description=m.Device.url.__doc__)
lots = NestedOn('Lot', many=True, dump_only=True)
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
def from_events_to_events_one(self, data: dict):
@ -60,6 +69,11 @@ class Device(Thing):
class Computer(Device):
components = NestedOn('Component', many=True, dump_only=True, collection_class=OrderedSet)
chassis = EnumField(ComputerChassis, required=True)
ram_size = Integer(dump_only=True, data_key='ramSize')
data_storage_size = Integer(dump_only=True, data_key='dataStorageSize')
processor_model = Str(dump_only=True, data_key='processorModel')
graphic_card_model = Str(dump_only=True, data_key='graphicCardModel')
network_speeds = List(Integer(dump_only=True), dump_only=True, data_key='networkSpeeds')
class Desktop(Computer):
@ -148,6 +162,7 @@ class DataStorage(Component):
unit=UnitCodes.mbyte,
description=m.DataStorage.size.comment)
interface = EnumField(DataStorageInterface)
privacy = EnumField(DataStoragePrivacyCompliance, dump_only=True)
class HardDrive(DataStorage):

View File

@ -108,6 +108,7 @@ class DeviceSearch(db.Model):
tags = session.query(
search.Search.vectorize(
(db.func.string_agg(Tag.id, ' '), search.Weight.A),
(db.func.string_agg(Tag.secondary, ' '), search.Weight.A),
(db.func.string_agg(Organization.name, ' '), search.Weight.B)
)
).filter(Tag.device_id == device.id).join(Tag.org)

View File

@ -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

View File

@ -235,3 +235,21 @@ class ReceiverRole(Enum):
CollectionPoint = 'A collection point.'
RecyclingPoint = 'A recycling point.'
Transporter = 'An user that ships the devices to another one.'
class DataStoragePrivacyCompliance(Enum):
EraseBasic = 'EraseBasic'
EraseBasicError = 'EraseBasicError'
EraseSectors = 'EraseSectors'
EraseSectorsError = 'EraseSectorsError'
Destruction = 'Destruction'
DestructionError = 'DestructionError'
@classmethod
def from_erase(cls, erasure) -> 'DataStoragePrivacyCompliance':
"""Returns the correct enum depending of the passed-in erasure."""
from ereuse_devicehub.resources.event.models import EraseSectors
if isinstance(erasure, EraseSectors):
return cls.EraseSectors if not erasure.error else cls.EraseSectorsError
else:
return cls.EraseBasic if not erasure.error else cls.EraseBasicError

View File

@ -64,19 +64,9 @@ class WorkbenchRateDef(RateDef):
SCHEMA = schemas.WorkbenchRate
class PhotoboxUserDef(RateDef):
class ManualRateDef(RateDef):
VIEW = None
SCHEMA = schemas.PhotoboxUserRate
class PhotoboxSystemRateDef(RateDef):
VIEW = None
SCHEMA = schemas.PhotoboxSystemRate
class AppRateDef(RateDef):
VIEW = None
SCHEMA = schemas.AppRate
SCHEMA = schemas.ManualRate
class PriceDef(EventDef):
@ -98,7 +88,8 @@ class SnapshotDef(EventDef):
VIEW = SnapshotView
SCHEMA = schemas.Snapshot
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None, static_url_path=None,
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,

View File

@ -1,5 +1,7 @@
from collections import Iterable
from datetime import datetime, timedelta
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_UP
from distutils.version import StrictVersion
from typing import Set, Union
from uuid import uuid4
@ -24,17 +26,12 @@ from ereuse_devicehub.db import db
from ereuse_devicehub.resources.agent.models import Agent
from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \
Device, Laptop, Server
from ereuse_devicehub.resources.enums import AppearanceRange, BOX_RATE_3, BOX_RATE_5, Bios, \
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, \
FunctionalityRange, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, RatingRange, RatingSoftware, \
ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength
from ereuse_devicehub.resources.image.models import Image
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
"""
A quantity of money with a currency.
"""
class JoinedTableMixin:
# noinspection PyMethodParameters
@ -54,7 +51,7 @@ class Event(Thing):
incidence.comment = """
Should this event be reviewed due some anomaly?
"""
closed = Column(Boolean, default=False, nullable=False)
closed = Column(Boolean, default=True, nullable=False)
closed.comment = """
Whether the author has finished the event.
After this is set to True, no modifications are allowed.
@ -360,8 +357,11 @@ class SnapshotRequest(db.Model):
class Rate(JoinedWithOneDeviceMixin, EventWithOneDevice):
rating = Column(Float(decimal_return_scale=2), check_range('rating', *RATE_POSITIVE))
rating.comment = """The rating for the content."""
software = Column(DBEnum(RatingSoftware))
software.comment = """The algorithm used to produce this rating."""
version = Column(StrictVersionType)
version.comment = """The version of the software."""
appearance = Column(Float(decimal_return_scale=2), check_range('appearance', *RATE_NEGATIVE))
functionality = Column(Float(decimal_return_scale=2),
check_range('functionality', *RATE_NEGATIVE))
@ -389,35 +389,16 @@ class IndividualRate(Rate):
pass
class AggregateRate(Rate):
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
ratings = relationship(IndividualRate,
backref=backref('aggregated_ratings',
lazy=True,
order_by=lambda: IndividualRate.created,
collection_class=OrderedSet),
secondary=lambda: RateAggregateRate.__table__,
order_by=lambda: IndividualRate.created,
collection_class=OrderedSet)
"""The ratings this aggregateRate aggregates."""
class RateAggregateRate(db.Model):
"""
Represents the ``many to many`` relationship between
``Rate`` and ``AggregateRate``.
"""
rate_id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
aggregate_rate_id = Column(UUID(as_uuid=True),
ForeignKey(AggregateRate.id),
primary_key=True)
class ManualRate(IndividualRate):
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
labelling = Column(Boolean)
labelling.comment = """Sets if there are labels stuck that should
be removed.
"""
appearance_range = Column(DBEnum(AppearanceRange))
appearance_range.comment = AppearanceRange.__doc__
functionality_range = Column(DBEnum(FunctionalityRange))
functionality_range.comment = FunctionalityRange.__doc__
class WorkbenchRate(ManualRate):
@ -428,63 +409,120 @@ class WorkbenchRate(ManualRate):
check_range('data_storage', *RATE_POSITIVE))
graphic_card = Column(Float(decimal_return_scale=2),
check_range('graphic_card', *RATE_POSITIVE))
bios = Column(DBEnum(Bios))
bios = Column(Float(decimal_return_scale=2),
check_range('bios', *RATE_POSITIVE))
bios_range = Column(DBEnum(Bios))
bios_range.comment = Bios.__doc__
# todo ensure for WorkbenchRate version and software are not None when inserting them
def ratings(self) -> Set['WorkbenchRate']:
def ratings(self):
"""
Computes all the possible rates taking this rating as a model.
Returns a set of ratings, including this one, which is mutated.
"""
from ereuse_rate.main import main
from ereuse_devicehub.resources.event.rate.main import main
return main(self, **app.config.get_namespace('WORKBENCH_RATE_'))
class AppRate(ManualRate):
pass
class PhotoboxRate(IndividualRate):
class AggregateRate(Rate):
id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), primary_key=True)
image_id = Column(UUID(as_uuid=True), ForeignKey(Image.id), nullable=False)
image = relationship(Image,
uselist=False,
cascade=CASCADE_OWN,
single_parent=True,
primaryjoin=Image.id == image_id)
manual_id = Column(UUID(as_uuid=True), ForeignKey(ManualRate.id))
manual_id.comment = """The ManualEvent used to generate this
aggregation, or None if none used.
# 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):
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)
# todo take value from LAST event (manual or workbench)
@property
def processor(self):
return self.workbench.processor
class PhotoboxSystemRate(PhotoboxRate):
id = Column(UUID(as_uuid=True), ForeignKey(PhotoboxRate.id), primary_key=True)
@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):
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):
SCALE = 4
ROUND = ROUND_HALF_EVEN
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.comment = """The software used to compute this price,
if the price was computed automatically. This field is None
if the price has been manually set.
"""
version = Column(StrictVersionType)
version.comment = """The version of the software, or None."""
rating_id = Column(UUID(as_uuid=True), ForeignKey(AggregateRate.id))
rating_id.comment = """The AggregateRate used to auto-compute
this price, if it has not been set manually."""
rating = relationship(AggregateRate,
backref=backref('price',
lazy=True,
@ -493,8 +531,17 @@ class Price(JoinedWithOneDeviceMixin, EventWithOneDevice):
primaryjoin=AggregateRate.id == rating_id)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.currency = self.currency or app.config['PRICE_CURRENCY']
if 'price' in kwargs:
assert isinstance(kwargs['price'], Decimal), 'Price must be a Decimal'
super().__init__(currency=kwargs.pop('currency', app.config['PRICE_CURRENCY']), **kwargs)
@classmethod
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
"""Returns a Decimal value with the correct scale for Price.price."""
if isinstance(value, float):
value = Decimal(value)
# equation from marshmallow.fields.Decimal
return value.quantize(Decimal((0, (1,), -cls.SCALE)), rounding=rounding)
class EreusePrice(Price):
@ -505,9 +552,10 @@ class EreusePrice(Price):
}
class Type:
def __init__(self, percentage, price) -> None:
def __init__(self, percentage: float, price: Decimal) -> None:
# see https://stackoverflow.com/a/29651462 for the - 0.005
self.amount = round(price * percentage - 0.005, 2)
self.amount = EreusePrice.to_price(price * Decimal(percentage))
self.percentage = EreusePrice.to_price(price * Decimal(percentage))
self.percentage = round(percentage - 0.005, 2)
class Service:
@ -543,20 +591,25 @@ class EreusePrice(Price):
}
SCHEMA[Server] = SCHEMA[Desktop]
def __init__(self, device, rating_range, role, price) -> None:
def __init__(self, device, rating_range, role, price: Decimal) -> None:
cls = device.__class__ if device.__class__ != Server else Desktop
rate = self.SCHEMA[cls][rating_range]
self.standard = EreusePrice.Type(rate['STD'][role], price)
self.warranty2 = EreusePrice.Type(rate['WR2'][role], price)
self.standard = EreusePrice.Type(rate[self.STANDARD][role], price)
if self.WARRANTY2 in rate:
self.warranty2 = EreusePrice.Type(rate[self.WARRANTY2][role], price)
def __init__(self, rating: AggregateRate, **kwargs) -> None:
if rating.rating_range == RatingRange.VERY_LOW:
raise ValueError('Cannot compute price for Range.VERY_LOW')
self.price = round(rating.rating * self.MULTIPLIER[rating.device.__class__], 2)
super().__init__(rating=rating, device=rating.device, **kwargs)
# We pass ROUND_UP strategy so price is always greater than what refurbisher... amounts
price = self.to_price(rating.rating * self.MULTIPLIER[rating.device.__class__], ROUND_UP)
super().__init__(rating=rating,
device=rating.device,
price=price,
software=kwargs.pop('software', app.config['PRICE_SOFTWARE']),
version=kwargs.pop('version', app.config['PRICE_VERSION']),
**kwargs)
self._compute()
self.software = self.software or app.config['PRICE_SOFTWARE']
self.version = self.version or app.config['PRICE_VERSION']
@orm.reconstructor
def _compute(self):
@ -567,9 +620,10 @@ class EreusePrice(Price):
self.refurbisher = self._service(self.Service.REFURBISHER)
self.retailer = self._service(self.Service.RETAILER)
self.platform = self._service(self.Service.PLATFORM)
self.warranty2 = round(self.refurbisher.warranty2.amount
+ self.retailer.warranty2.amount
+ self.platform.warranty2.amount, 2)
if hasattr(self.refurbisher, 'warranty2'):
self.warranty2 = round(self.refurbisher.warranty2.amount
+ self.retailer.warranty2.amount
+ self.platform.warranty2.amount, 2)
def _service(self, role):
return self.Service(self.device, self.rating.rating_range, role, self.price)

View File

@ -19,7 +19,6 @@ from ereuse_devicehub.resources.device.models import Component, Computer, Device
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, FunctionalityRange, \
PriceSoftware, RatingSoftware, ReceiverRole, SnapshotExpectedEvents, SnapshotSoftware, \
TestDataStorageLength
from ereuse_devicehub.resources.image.models import Image
from ereuse_devicehub.resources.models import Thing
from ereuse_devicehub.resources.user.models import User
@ -78,7 +77,7 @@ class EventWithOneDevice(Event):
class EventWithMultipleDevices(Event):
devices = ... # type: relationship
devices = ... # type: relationship
def __init__(self, id=None, name=None, incidence=None, closed=None, error=None,
description=None, start_time=None, end_time=None, snapshot=None, agent=None,
@ -147,6 +146,8 @@ class Rate(EventWithOneDevice):
rating = ... # type: Column
appearance = ... # type: Column
functionality = ... # type: Column
software = ... # type: Column
version = ... # type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
@ -165,59 +166,100 @@ class IndividualRate(Rate):
class AggregateRate(Rate):
manual_id = ... # type: Column
manual = ... # type: relationship
workbench = ... # type: relationship
workbench_id = ... # type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.ratings = ... # type: Set[IndividualRate]
self.manual_id = ... # type: UUID
self.manual = ... # type: ManualRate
self.workbench = ... # type: WorkbenchRate
self.workbench_id = ... # type: UUID
self.price = ... # type: Price
@property
def processor(self):
return self.workbench.processor
@property
def ram(self):
return self.workbench.ram
@property
def data_storage(self):
return self.workbench.data_storage
@property
def graphic_card(self):
return self.workbench.graphic_card
@property
def bios(self):
return self.workbench.bios
@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):
labelling = ... # type: Column
appearance_range = ... # type: Column
functionality_range = ... # type: Column
aggregate_rate_manual = ... #type: relationship
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.labelling = ... # type: bool
self.appearance_range = ... # type: AppearanceRange
self.functionality_range = ... # type: FunctionalityRange
self.aggregate_rate_manual = ... #type: AggregateRate
class WorkbenchRate(ManualRate):
processor = ... # type: Column
ram = ... # type: Column
data_storage = ... # type: Column
graphic_card = ... # type: Column
bios_range = ... # type: Column
bios = ... # type: Column
aggregate_rate_workbench = ... #type: Column
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.processor = ... # type: float
self.ram = ... # type: float
self.data_storage = ... # type: float
self.graphic_card = ... # type: float
self.bios = ... # type: Bios
self.bios_range = ... # type: Bios
self.bios = ... # type: float
self.aggregate_rate_workbench = ... #type: AggregateRate
class AppRate(ManualRate):
pass
class PhotoboxRate(IndividualRate):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.num = ... # type: int
self.image = ... # type: Image
class PhotoboxUserRate(PhotoboxRate):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.assembling = ... # type: int
self.parts = ... # type: int
self.buttons = ... # type: int
self.dents = ... # type: int
self.decolorization = ... # type: int
self.scratches = ... # type: int
self.tag_adhesive = ... # type: int
self.dirt = ... # type: int
class PhotoboxSystemRate(PhotoboxRate):
pass
def ratings(self) -> Set[Rate]:
pass
class Price(EventWithOneDevice):
SCALE = ...
ROUND = ...
currency = ... # type: Column
price = ... # type: Column
software = ... # type: Column
@ -233,12 +275,32 @@ class Price(EventWithOneDevice):
self.version = ... # type: StrictVersion
self.rating = ... # type: AggregateRate
@classmethod
def to_price(cls, value: Union[Decimal, float], rounding=ROUND) -> Decimal:
pass
class EreusePrice(Price):
MULTIPLIER = ... # type: Dict
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:
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):

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -1,5 +1,3 @@
import decimal
from flask import current_app as app
from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema
from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, List, Nested, String, \
@ -101,62 +99,30 @@ class StepRandom(Step):
class Rate(EventWithOneDevice):
rating = Integer(validate=Range(*RATE_POSITIVE),
dump_only=True,
data_key='ratingValue',
description='The rating for the content.')
description=m.Rate.rating.comment)
software = EnumField(RatingSoftware,
dump_only=True,
description='The algorithm used to produce this rating.')
description=m.Rate.software.comment)
version = Version(dump_only=True,
description='The version of the software.')
description=m.Rate.version.comment)
appearance = Integer(validate=Range(-3, 5), dump_only=True)
functionality = Integer(validate=Range(-3, 5),
dump_only=True,
data_key='functionalityScore')
functionality = Integer(validate=Range(-3, 5), dump_only=True)
class IndividualRate(Rate):
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):
appearance_range = EnumField(AppearanceRange,
required=True,
data_key='appearanceRange',
description='Grades the imperfections that aesthetically '
'affect the device, but not its usage.')
description=m.ManualRate.appearance_range.comment)
functionality_range = EnumField(FunctionalityRange,
required=True,
data_key='functionalityRange',
description='Grades the defects of a device affecting usage.')
labelling = Boolean(description='Sets if there are labels stuck that should be removed.')
class AppRate(ManualRate):
pass
description=m.ManualRate.functionality_range.comment)
labelling = Boolean(description=m.ManualRate.labelling.comment)
class WorkbenchRate(ManualRate):
@ -164,16 +130,46 @@ class WorkbenchRate(ManualRate):
ram = Float()
data_storage = Float()
graphic_card = Float()
bios = EnumField(Bios, description='How difficult it has been to set the bios to '
'boot from the network.')
bios = Float()
bios_range = EnumField(Bios,
description=m.WorkbenchRate.bios_range.comment,
data_key='biosRange')
class AggregateRate(Rate):
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):
currency = EnumField(Currency, required=True)
price = Decimal(places=4, rounding=decimal.ROUND_HALF_EVEN, required=True)
software = EnumField(PriceSoftware, dump_only=True)
version = Version(dump_only=True)
rating = NestedOn(AggregateRate, dump_only=True)
currency = EnumField(Currency, required=True, description=m.Price.currency.comment)
price = Decimal(places=m.Price.SCALE,
rounding=m.Price.ROUND,
required=True,
description=m.Price.price.comment)
software = EnumField(PriceSoftware, dump_only=True, description=m.Price.software.comment)
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):
@ -285,7 +281,7 @@ class StressTest(Test):
class Benchmark(EventWithOneDevice):
elapsed = TimeDelta(precision=TimeDelta.SECONDS)
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)
class BenchmarkDataStorage(Benchmark):

View File

@ -1,4 +1,3 @@
from contextlib import suppress
from distutils.version import StrictVersion
from typing import List
from uuid import UUID
@ -77,11 +76,9 @@ class SnapshotView(View):
snapshot.events |= events
# Compute ratings
with suppress(StopIteration):
# todo are we sure we want to have snapshots without rates?
snapshot.events |= next(
e.ratings() for e in events_device if isinstance(e, WorkbenchRate)
)
for rate in (e for e in events_device if isinstance(e, WorkbenchRate)):
rates = rate.ratings()
snapshot.events |= rates
db.session.add(snapshot)
db.session.commit()

View File

@ -15,7 +15,8 @@ class LotDef(Resource):
AUTH = True
ID_CONVERTER = Converters.uuid
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None, static_url_path=None,
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None, cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
super().__init__(app, import_name, static_folder, static_url_path, template_folder,

View File

@ -75,8 +75,8 @@ class Tag(Thing):
return url
__table_args__ = (
UniqueConstraint(device_id, org_id, name='one_tag_per_org'),
UniqueConstraint(secondary, org_id, name='one_secondary_per_org')
UniqueConstraint(id, org_id, name='one tag id per organization'),
UniqueConstraint(secondary, org_id, name='one secondary tag per organization')
)
def __repr__(self) -> str:

View File

@ -5,7 +5,6 @@ click==6.7
click-spinner==0.1.8
colorama==0.3.9
colour==0.1.5
ereuse-rate==0.0.2
ereuse-utils==0.4.0b9
Flask==1.0.2
Flask-Cors==3.0.6

View File

@ -37,7 +37,6 @@ setup(
'teal>=0.2.0a24', # teal always first
'click',
'click-spinner',
'ereuse-rate==0.0.2',
'ereuse-utils[Naming]>=0.4b9',
'hashids',
'marshmallow_enum',

View File

@ -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"
}

View File

@ -14,7 +14,7 @@ device:
appearanceRange: A
functionalityRange: B
labelling: True
bios: B
biosRange: B
components:
- type: GraphicCard
serialNumber: gc1s
@ -33,3 +33,4 @@ components:
events:
- type: BenchmarkProcessor
rate: 2410
elapsed: 11

View File

@ -39,4 +39,4 @@ def test_api_docs(client: Client):
'scheme': 'basic',
'name': 'Authorization'
}
assert 77 == len(docs['definitions'])
assert 75 == len(docs['definitions'])

View File

@ -91,7 +91,14 @@ def test_physical_properties():
manufacturer='mr',
width=2.0,
color=Color())
pc = Desktop(chassis=ComputerChassis.Tower)
pc = Desktop(chassis=ComputerChassis.Tower,
model='foo',
manufacturer='bar',
serial_number='foo-bar',
weight=2.8,
width=1.4,
height=2.1,
color=Color('LightSeaGreen'))
pc.components.add(c)
db.session.add(pc)
db.session.commit()
@ -110,6 +117,17 @@ def test_physical_properties():
'color': Color(),
'depth': None
}
assert pc.physical_properties == {
'model': 'foo',
'manufacturer': 'bar',
'serial_number': 'foo-bar',
'weight': 2.8,
'width': 1.4,
'height': 2.1,
'depth': None,
'color': Color('LightSeaGreen'),
'chassis': ComputerChassis.Tower
}
@pytest.mark.usefixtures(conftest.app_context.__name__)

View File

@ -1,5 +1,7 @@
import ipaddress
from datetime import timedelta
from decimal import Decimal
from typing import Tuple
import pytest
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.db import db
from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
RamModule, SolidStateDrive
from ereuse_devicehub.resources.enums import ComputerChassis, TestDataStorageLength
@ -175,22 +178,24 @@ def test_update_parent():
assert not benchmark.parent
@pytest.mark.parametrize('event_model', [
models.ToRepair,
models.Repair,
models.ToPrepare,
models.ReadyToUse,
models.ToPrepare,
models.Prepare,
@pytest.mark.parametrize('event_model_state', [
(models.ToRepair, states.Physical.ToBeRepaired),
(models.Repair, states.Physical.Repaired),
(models.ToPrepare, states.Physical.Preparing),
(models.ReadyToUse, states.Physical.ReadyToBeUsed),
(models.ToPrepare, states.Physical.Preparing),
(models.Prepare, states.Physical.Prepared)
])
def test_generic_event(event_model: models.Event, user: UserClient):
def test_generic_event(event_model_state: Tuple[models.Event, states.Trading], user: UserClient):
"""Tests POSTing all generic events."""
event_model, state = event_model_state
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
event = {'type': event_model.t, 'devices': [snapshot['device']['id']]}
event, _ = user.post(event, res=models.Event)
assert event['devices'][0]['id'] == snapshot['device']['id']
device, _ = user.get(res=Device, item=snapshot['device']['id'])
assert device['events'][-1]['id'] == event['id']
assert device['physical'] == state.name
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
@ -214,6 +219,8 @@ def test_live():
assert live['ip'] == '79.147.10.10'
assert live['subdivision'] == 'ES-CA'
assert live['country'] == 'ES'
device, _ = client.get(res=Device, item=live['device']['id'])
assert device['physical'] == states.Physical.InUse.name
@pytest.mark.xfail(reson='Functionality not developed.')
@ -226,14 +233,15 @@ def test_reserve(user: UserClient):
"""Performs a reservation and then cancels it."""
@pytest.mark.parametrize('event_model', [
models.Sell,
models.Donate,
models.Rent,
models.DisposeProduct
@pytest.mark.parametrize('event_model_state', [
(models.Sell, states.Trading.Sold),
(models.Donate, states.Trading.Donated),
(models.Rent, states.Trading.Renting),
(models.DisposeProduct, states.Trading.ProductDisposed)
])
def test_trade(event_model: models.Event, user: UserClient):
"""Tests POSTing all generic events."""
def test_trade(event_model_state: Tuple[models.Event, states.Trading], user: UserClient):
"""Tests POSTing all Trade events."""
event_model, state = event_model_state
snapshot, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
event = {
'type': event_model.t,
@ -246,6 +254,7 @@ def test_trade(event_model: models.Event, user: UserClient):
assert event['devices'][0]['id'] == snapshot['device']['id']
device, _ = user.get(res=Device, item=snapshot['device']['id'])
assert device['events'][-1]['id'] == event['id']
assert device['trading'] == state.name
@pytest.mark.xfail(reson='Develop migrate')
@ -257,8 +266,9 @@ def test_migrate():
def test_price_custom():
computer = Desktop(serial_number='sn1', model='ml1', manufacturer='mr1',
chassis=ComputerChassis.Docking)
price = models.Price(price=25.25, currency=Currency.EUR)
price = models.Price(price=Decimal(25.25), currency=Currency.EUR)
price.device = computer
assert computer.price == price
db.session.add(computer)
db.session.commit()
@ -268,3 +278,15 @@ def test_price_custom():
assert p['device']['id'] == price.device.id == computer.id
assert p['price'] == 25.25
assert p['currency'] == Currency.EUR.name == 'EUR'
c, _ = client.get(res=Device, item=computer.id)
assert c['price']['id'] == p['id']
@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

View File

@ -254,7 +254,7 @@ def test_post_get_lot(user: UserClient):
assert not l['children']
def test_post_add_children_view_ui_tree_normal(user: UserClient):
def test_lot_post_add_children_view_ui_tree_normal(user: UserClient):
"""Tests adding children lots to a lot through the view and
GETting the results."""
parent, _ = user.post(({'name': 'Parent'}), res=Lot)

View File

@ -1,13 +1,16 @@
from decimal import Decimal
from distutils.version import StrictVersion
import pytest
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Computer, Desktop
from ereuse_devicehub.resources.enums import Bios, ComputerChassis, ImageMimeTypes, Orientation, \
RatingSoftware
from ereuse_devicehub.resources.event.models import PhotoboxRate, WorkbenchRate
from ereuse_devicehub.resources.image.models import Image, ImageList
from ereuse_devicehub.resources.device.models import Computer, Desktop, HardDrive, Processor, \
RamModule
from ereuse_devicehub.resources.enums import AppearanceRange, Bios, ComputerChassis, \
FunctionalityRange, RatingSoftware
from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkDataStorage, \
BenchmarkProcessor, EreusePrice, WorkbenchRate
from ereuse_devicehub.resources.event.rate import main
from tests import conftest
@ -15,7 +18,7 @@ from tests import conftest
def test_workbench_rate_db():
rate = WorkbenchRate(processor=0.1,
ram=1.0,
bios=Bios.A,
bios_range=Bios.A,
labelling=False,
graphic_card=0.1,
data_storage=4.1,
@ -26,17 +29,63 @@ def test_workbench_rate_db():
db.session.commit()
@pytest.mark.usefixtures(conftest.auth_app_context.__name__)
def test_photobox_rate_db():
pc = Desktop(serial_number='24', chassis=ComputerChassis.Tower)
image = Image(name='foo',
content=b'123',
file_format=ImageMimeTypes.jpg,
orientation=Orientation.Horizontal,
image_list=ImageList(device=pc))
rate = PhotoboxRate(image=image,
software=RatingSoftware.ECost,
version=StrictVersion('1.0'),
device=pc)
db.session.add(rate)
db.session.commit()
@pytest.mark.xfail(reason='AggreagteRate only takes data from WorkbenchRate as for now')
def test_rate_workbench_then_manual():
"""Checks that a new AggregateRate is generated with a new rate
value when a ManualRate is performed after performing a
WorkbenchRate.
The new AggregateRate needs to be computed by the values of
the WorkbenchRate + new values from ManualRate.
"""
pass
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_rate():
"""Test generating an AggregateRate for a given PC / components /
WorkbenchRate ensuring results and relationships between
pc - rate - workbenchRate - price.
"""
rate = WorkbenchRate(
appearance_range=AppearanceRange.A,
functionality_range=FunctionalityRange.A
)
pc = Desktop()
hdd = HardDrive(size=476940)
hdd.events_one.add(BenchmarkDataStorage(read_speed=126, write_speed=29.8))
cpu = Processor(cores=2, speed=3.4)
cpu.events_one.add(BenchmarkProcessor(rate=27136.44))
pc.components |= {
hdd,
RamModule(size=4096, speed=1600),
RamModule(size=2048, speed=1067),
cpu
}
rate.device = pc
events = main.main(rate, RatingSoftware.ECost, StrictVersion('1.0'))
price = next(e for e in events if isinstance(e, EreusePrice))
assert price.price == 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

View File

@ -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."""

View File

@ -1,9 +1,9 @@
from datetime import datetime, timedelta, timezone
from distutils.version import StrictVersion
from typing import List, Tuple
from uuid import uuid4
import pytest
from boltons import urlutils
from teal.db import UniqueViolation
from teal.marshmallow import ValidationError
@ -13,8 +13,7 @@ from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.device import models as m
from ereuse_devicehub.resources.device.exceptions import NeedsId
from ereuse_devicehub.resources.device.sync import MismatchBetweenTagsAndHid
from ereuse_devicehub.resources.enums import Bios, ComputerChassis, RatingSoftware, \
SnapshotSoftware
from ereuse_devicehub.resources.enums import ComputerChassis, SnapshotSoftware
from ereuse_devicehub.resources.event.models import AggregateRate, BenchmarkProcessor, \
EraseSectors, Event, Snapshot, SnapshotRequest, WorkbenchRate
from ereuse_devicehub.resources.tag import Tag
@ -37,21 +36,11 @@ def test_snapshot_model():
elapsed=timedelta(seconds=25))
snapshot.device = device
snapshot.request = SnapshotRequest(request={'foo': 'bar'})
snapshot.events.add(WorkbenchRate(processor=0.1,
ram=1.0,
bios=Bios.A,
labelling=False,
graphic_card=0.1,
data_storage=4.1,
software=RatingSoftware.ECost,
version=StrictVersion('1.0'),
device=device))
db.session.add(snapshot)
db.session.commit()
device = m.Desktop.query.one() # type: m.Desktop
e1, e2 = device.events
e1 = device.events[0]
assert isinstance(e1, Snapshot), 'Creation order must be preserved: 1. snapshot, 2. WR'
assert isinstance(e2, WorkbenchRate)
db.session.delete(device)
db.session.commit()
assert Snapshot.query.one_or_none() is None
@ -59,6 +48,8 @@ def test_snapshot_model():
assert User.query.one() is not None
assert m.Desktop.query.one_or_none() is None
assert m.Device.query.one_or_none() is None
# Check properties
assert device.url == urlutils.URL('http://localhost/devices/1')
def test_snapshot_schema(app: Devicehub):
@ -321,27 +312,37 @@ def test_erase(user: UserClient):
assert step['type'] == 'StepZero'
assert step['error'] is False
assert 'num' not in step
assert storage['privacy'] == erasure['device']['privacy'] == 'EraseSectors'
# Let's try a second erasure with an error
s['uuid'] = uuid4()
s['components'][0]['events'][0]['error'] = True
snapshot, _ = user.post(s, res=Snapshot)
assert snapshot['components'][0]['hid'] == 'c1mr-c1s-c1ml'
assert snapshot['components'][0]['privacy'] == 'EraseSectorsError'
def test_snapshot_computer_monitor(user: UserClient):
s = file('computer-monitor.snapshot')
snapshot_and_check(user, s, event_types=('AppRate',))
snapshot_and_check(user, s, event_types=('ManualRate',))
# todo check that ManualRate has generated an AggregateRate
def test_snapshot_mobile_smartphone(user: UserClient):
s = file('smartphone.snapshot')
snapshot_and_check(user, s, event_types=('AppRate',))
snapshot_and_check(user, s, event_types=('ManualRate',))
# todo check that ManualRate has generated an AggregateRate
@pytest.mark.xfail(reason='Test not developed')
def test_snapshot_components_none():
"""
Tests that a snapshot without components does not
remove them from the computer.
"""
# todo test
pass
@pytest.mark.xfail(reason='Test not developed')
def test_snapshot_components_empty():
"""
Tests that a snapshot whose components are an empty list remove

View File

@ -12,10 +12,12 @@ from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.agent.models import Organization
from ereuse_devicehub.resources.device.models import Desktop, Device
from ereuse_devicehub.resources.enums import ComputerChassis
from ereuse_devicehub.resources.event.models import Snapshot
from ereuse_devicehub.resources.tag import Tag
from ereuse_devicehub.resources.tag.view import CannotCreateETag, LinkedToAnotherDevice, \
TagNotLinked
from tests import conftest
from tests.conftest import file
@pytest.mark.usefixtures(conftest.app_context.__name__)
@ -179,8 +181,10 @@ def test_tag_manual_link(app: Devicehub, user: UserClient):
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_tag_secondary():
"""Creates and consumes tags with a secondary id."""
def test_tag_secondary_workbench_link_find(user: UserClient):
"""Creates and consumes tags with a secondary id, linking them
through Workbench to a device
and getting them through search."""
t = Tag('foo', secondary='bar')
db.session.add(t)
db.session.flush()
@ -189,6 +193,18 @@ def test_tag_secondary():
with pytest.raises(ResourceNotFound):
Tag.from_an_id('nope').one()
s = file('basic.snapshot')
s['device']['tags'] = [{'id': 'foo', 'secondary': 'bar', 'type': 'Tag'}]
snapshot, _ = user.post(s, res=Snapshot)
device, _ = user.get(res=Device, item=snapshot['device']['id'])
assert device['tags'][0]['id'] == 'foo'
assert device['tags'][0]['secondary'] == 'bar'
r, _ = user.get(res=Device, query=[('search', 'foo'), ('filter', {'type': ['Computer']})])
assert len(r['items']) == 1
r, _ = user.get(res=Device, query=[('search', 'bar'), ('filter', {'type': ['Computer']})])
assert len(r['items']) == 1
def test_tag_create_tags_cli_csv(app: Devicehub, user: UserClient):
"""Checks creating tags with the CLI endpoint using a CSV."""

View File

@ -27,7 +27,7 @@ def test_workbench_server_condensed(user: UserClient):
file('workbench-server-3.erase'),
file('workbench-server-4.install')
))
s['components'][5]['events'] = [file('workbench-server-3.erase')]
s['components'][5]['events'].append(file('workbench-server-3.erase'))
# Create tags
for t in s['device']['tags']:
user.post({'id': t['id']}, res=Tag)
@ -35,7 +35,7 @@ def test_workbench_server_condensed(user: UserClient):
snapshot, _ = user.post(res=em.Snapshot, data=s)
events = snapshot['events']
assert {(event['type'], event['device']) for event in events} == {
# todo missing Rate event aggregating the rates
('AggregateRate', 1),
('WorkbenchRate', 1),
('BenchmarkProcessorSysbench', 5),
('StressTest', 1),
@ -45,10 +45,26 @@ def test_workbench_server_condensed(user: UserClient):
('Install', 6),
('EraseSectors', 7),
('BenchmarkDataStorage', 6),
('BenchmarkDataStorage', 7),
('TestDataStorage', 6)
}
assert snapshot['closed']
assert not snapshot['error']
device, _ = user.get(res=Device, item=snapshot['device']['id'])
assert device['dataStorageSize'] == 1100
assert device['chassis'] == 'Tower'
assert device['hid'] == 'd1mr-d1s-d1ml'
assert device['graphicCardModel'] == device['components'][0]['model'] == 'gc1-1ml'
assert device['networkSpeeds'] == [1000, 58]
assert device['processorModel'] == device['components'][3]['model'] == 'p1-1ml'
assert device['ramSize'] == 2048, 'There are 3 RAM: 2 x 1024 and 1 None sizes'
assert device['rate']['closed']
assert not device['rate']['error']
assert device['rate']['rating'] == 0
assert device['rate']['workbench']
assert device['rate']['appearanceRange'] == 'A'
assert device['rate']['functionalityRange'] == 'B'
assert device['tags'][0]['id'] == 'tag1'
@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):
s = file('real-hp.snapshot.11')
snapshot, _ = user.post(res=em.Snapshot, data=s)
assert snapshot['device']['hid'] == 'hewlett-packard-czc0408yjg-hp_compaq_8100_elite_sff'
assert snapshot['device']['chassis'] == 'Tower'
pc = snapshot['device']
assert pc['hid'] == 'hewlett-packard-czc0408yjg-hp_compaq_8100_elite_sff'
assert pc['chassis'] == 'Tower'
assert set(e['type'] for e in snapshot['events']) == {
'BenchmarkDataStorage',
'BenchmarkProcessor',
@ -133,6 +150,10 @@ def test_real_hp_11(user: UserClient):
'StressTest'
}
assert len(list(e['type'] for e in snapshot['events'])) == 6
assert pc['networkSpeeds'] == [1000, None], 'Device has no WiFi'
assert pc['processorModel'] == 'intel core i3 cpu 530 @ 2.93ghz'
assert pc['ramSize'] == 8192
assert pc['dataStorageSize'] == 305245
def test_real_toshiba_11(user: UserClient):
@ -140,7 +161,7 @@ def test_real_toshiba_11(user: UserClient):
snapshot, _ = user.post(res=em.Snapshot, data=s)
def test_real_eee_1001pxd(user: UserClient):
def test_snapshot_real_eee_1001pxd(user: UserClient):
"""
Checks the values of the device, components,
events and their relationships of a real pc.
@ -155,6 +176,7 @@ def test_real_eee_1001pxd(user: UserClient):
assert pc['manufacturer'] == 'asustek computer inc.'
assert pc['hid'] == 'asustek_computer_inc-b8oaas048286-1001pxd'
assert pc['tags'] == []
assert pc['networkSpeeds'] == [100, 0], 'Although it has WiFi we do not know the speed'
components = snapshot['components']
wifi = components[0]
assert wifi['hid'] == 'qualcomm_atheros-74_2f_68_8b_fd_c8-ar9285_wireless_network_adapter'
@ -170,7 +192,7 @@ def test_real_eee_1001pxd(user: UserClient):
assert cpu['threads'] == 1
assert cpu['speed'] == 1.667
assert 'hid' not in cpu
assert cpu['model'] == 'intel atom cpu n455 @ 1.66ghz'
assert pc['processorModel'] == cpu['model'] == 'intel atom cpu n455 @ 1.66ghz'
cpu, _ = user.get(res=Device, item=cpu['id'])
events = cpu['events']
sysbench = next(e for e in events if e['type'] == em.BenchmarkProcessorSysbench.t)
@ -204,6 +226,7 @@ def test_real_eee_1001pxd(user: UserClient):
ram = components[6]
assert ram['interface'] == 'DDR2'
assert ram['speed'] == 667
assert pc['ramSize'] == ram['size'] == 1024
hdd = components[7]
assert hdd['type'] == 'HardDrive'
assert hdd['hid'] == 'hitachi-e2024242cv86hj-hts54322'
@ -223,6 +246,7 @@ def test_real_eee_1001pxd(user: UserClient):
assert erase['startTime']
assert erase['zeros'] is False
assert erase['error'] is False
assert hdd['privacy'] == 'EraseBasic'
mother = components[8]
assert mother['hid'] == 'asustek_computer_inc-eee0123456789-1001pxd'
@ -243,6 +267,11 @@ def test_real_eee_1000h(user: UserClient):
snapshot, _ = user.post(res=em.Snapshot, data=s)
@pytest.mark.xfail(reason='We do not have a snapshot file to use')
def test_real_full_with_workbench_rate(user: UserClient):
pass
SNAPSHOTS_NEED_ID = {
'box-xavier.snapshot.json',
'custom.lshw.snapshot.json',
@ -267,3 +296,9 @@ def test_workbench_fixtures(file: pathlib.Path, user: UserClient):
user.post(res=em.Snapshot,
data=s,
status=201 if file.name not in SNAPSHOTS_NEED_ID else NeedsId)
def test_workbench_asus_1001pxd_rate_low(user: UserClient):
"""Tests an Asus 1001pxd with a low rate."""
s = file('asus-1001pxd.snapshot')
snapshot, _ = user.post(res=em.Snapshot, data=s)