diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index be15650d..36c2dbd7 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -21,6 +21,7 @@ class DevicehubConfig(Config): import_resource(agent), import_resource(lot), import_resource(deliverynote), + import_resource(proof), import_resource(documents), import_resource(inventory)), ) diff --git a/ereuse_devicehub/resources/proof/__init__.py b/ereuse_devicehub/resources/proof/__init__.py new file mode 100644 index 00000000..7f6e6232 --- /dev/null +++ b/ereuse_devicehub/resources/proof/__init__.py @@ -0,0 +1,279 @@ +from typing import Callable, Iterable, Tuple + +from teal.resource import Converters, Resource + +from ereuse_devicehub.resources.action import schemas +from ereuse_devicehub.resources.action.views import ActionView +from ereuse_devicehub.resources.device.sync import Sync + + +class ProofDef(Resource): + SCHEMA = schemas.Proof + VIEW = ProofView + AUTH = True + ID_CONVERTER = Converters.uuid + + +class ActionDef(Resource): + SCHEMA = schemas.Action + VIEW = ActionView + AUTH = True + ID_CONVERTER = Converters.uuid + + +class AddDef(ActionDef): + VIEW = None + SCHEMA = schemas.Add + + +class RemoveDef(ActionDef): + VIEW = None + SCHEMA = schemas.Remove + + +class EraseBasicDef(ActionDef): + VIEW = None + SCHEMA = schemas.EraseBasic + + +class EraseSectorsDef(EraseBasicDef): + VIEW = None + SCHEMA = schemas.EraseSectors + + +class ErasePhysicalDef(EraseBasicDef): + VIEW = None + SCHEMA = schemas.ErasePhysical + + +class StepDef(Resource): + VIEW = None + SCHEMA = schemas.Step + + +class StepZeroDef(StepDef): + VIEW = None + SCHEMA = schemas.StepZero + + +class StepRandomDef(StepDef): + VIEW = None + SCHEMA = schemas.StepRandom + + +class BenchmarkDef(ActionDef): + VIEW = None + SCHEMA = schemas.Benchmark + + +class BenchmarkDataStorageDef(BenchmarkDef): + VIEW = None + SCHEMA = schemas.BenchmarkDataStorage + + +class BenchmarkWithRateDef(BenchmarkDef): + VIEW = None + SCHEMA = schemas.BenchmarkWithRate + + +class BenchmarkProcessorDef(BenchmarkWithRateDef): + VIEW = None + SCHEMA = schemas.BenchmarkProcessor + + +class BenchmarkProcessorSysbenchDef(BenchmarkProcessorDef): + VIEW = None + SCHEMA = schemas.BenchmarkProcessorSysbench + + +class BenchmarkRamSysbenchDef(BenchmarkWithRateDef): + VIEW = None + SCHEMA = schemas.BenchmarkRamSysbench + + +class TestDef(ActionDef): + VIEW = None + SCHEMA = schemas.Test + + +class MeasureBattery(TestDef): + VIEW = None + SCHEMA = schemas.MeasureBattery + + +class TestDataStorageDef(TestDef): + VIEW = None + SCHEMA = schemas.TestDataStorage + + +class StressTestDef(TestDef): + VIEW = None + SCHEMA = schemas.StressTest + + +class TestAudioDef(TestDef): + VIEW = None + SCHEMA = schemas.TestAudio + + +class TestConnectivityDef(TestDef): + VIEW = None + SCHEMA = schemas.TestConnectivity + + +class TestCameraDef(TestDef): + VIEW = None + SCHEMA = schemas.TestCamera + + +class TestKeyboardDef(TestDef): + VIEW = None + SCHEMA = schemas.TestKeyboard + + +class TestTrackpadDef(TestDef): + VIEW = None + SCHEMA = schemas.TestTrackpad + + +class TestBiosDef(TestDef): + VIEW = None + SCHEMA = schemas.TestBios + + +class VisualTestDef(TestDef): + VIEW = None + SCHEMA = schemas.VisualTest + + +class RateDef(ActionDef): + VIEW = None + SCHEMA = schemas.Rate + + +class RateComputerDef(RateDef): + VIEW = None + SCHEMA = schemas.RateComputer + + +class PriceDef(ActionDef): + VIEW = None + SCHEMA = schemas.Price + + +class EreusePriceDef(ActionDef): + VIEW = None + SCHEMA = schemas.EreusePrice + + +class InstallDef(ActionDef): + VIEW = None + SCHEMA = schemas.Install + + +class SnapshotDef(ActionDef): + VIEW = None + SCHEMA = schemas.Snapshot + + 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()): + url_prefix = '/{}'.format(ActionDef.resource) + super().__init__(app, import_name, static_folder, static_url_path, template_folder, + url_prefix, subdomain, url_defaults, root_path, cli_commands) + self.sync = Sync() + + +class ToRepairDef(ActionDef): + VIEW = None + SCHEMA = schemas.ToRepair + + +class RepairDef(ActionDef): + VIEW = None + SCHEMA = schemas.Repair + + +class ReadyDef(ActionDef): + VIEW = None + SCHEMA = schemas.Ready + + +class ToPrepareDef(ActionDef): + VIEW = None + SCHEMA = schemas.ToPrepare + + +class PrepareDef(ActionDef): + VIEW = None + SCHEMA = schemas.Prepare + + +class LiveDef(ActionDef): + VIEW = None + SCHEMA = schemas.Live + + +class ReserveDef(ActionDef): + VIEW = None + SCHEMA = schemas.Reserve + + +class CancelReservationDef(ActionDef): + VIEW = None + SCHEMA = schemas.CancelReservation + + +class SellDef(ActionDef): + VIEW = None + SCHEMA = schemas.Sell + + +class DonateDef(ActionDef): + VIEW = None + SCHEMA = schemas.Donate + + +class RentDef(ActionDef): + VIEW = None + SCHEMA = schemas.Rent + + +class MakeAvailable(ActionDef): + VIEW = None + SCHEMA = schemas.MakeAvailable + + +class CancelTradeDef(ActionDef): + VIEW = None + SCHEMA = schemas.CancelTrade + + +class ToDisposeProductDef(ActionDef): + VIEW = None + SCHEMA = schemas.ToDisposeProduct + + +class DisposeProductDef(ActionDef): + VIEW = None + SCHEMA = schemas.DisposeProduct + + +class ReceiveDef(ActionDef): + VIEW = None + SCHEMA = schemas.Receive + + +class MigrateToDef(ActionDef): + VIEW = None + SCHEMA = schemas.MigrateTo + + +class MigrateFromDef(ActionDef): + VIEW = None + SCHEMA = schemas.MigrateFrom + +class TransferredDef(ActionDef): + VIEW = None + SCHEMA = schemas.Transferred diff --git a/ereuse_devicehub/resources/proof/models.py b/ereuse_devicehub/resources/proof/models.py new file mode 100644 index 00000000..ec6ed171 --- /dev/null +++ b/ereuse_devicehub/resources/proof/models.py @@ -0,0 +1,127 @@ +"""This file contains all proofs related to actions + +""" + +from collections import Iterable +from contextlib import suppress +from datetime import datetime, timedelta, timezone +from decimal import Decimal, ROUND_HALF_EVEN, ROUND_UP +from typing import Optional, Set, Union +from uuid import uuid4 + +import inflection +import teal.db +from boltons import urlutils +from citext import CIText +from flask import current_app as app, g +from sortedcontainers import SortedSet +from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, Enum as DBEnum, \ + Float, ForeignKey, Integer, Interval, JSON, Numeric, SmallInteger, Unicode, event, orm +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.orderinglist import ordering_list +from sqlalchemy.orm import backref, relationship, validates +from sqlalchemy.orm.events import AttributeEvents as Events +from sqlalchemy.util import OrderedSet +from teal.db import CASCADE_OWN, INHERIT_COND, IP, POLYMORPHIC_ID, \ + POLYMORPHIC_ON, StrictVersionType, URL, check_lower, check_range +from teal.enums import Country, Currency, Subdivision +from teal.marshmallow import ValidationError +from teal.resource import url_for_resource + +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, BatteryHealth, BiosAccessRange, \ + ErasureStandards, FunctionalityRange, PhysicalErasureMethod, PriceSoftware, \ + R_NEGATIVE, R_POSITIVE, RatingRange, ReceiverRole, Severity, SnapshotSoftware, \ + TestDataStorageLength +from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing +from ereuse_devicehub.resources.user.models import User + + +class JoinedTableMixin: + # noinspection PyMethodParameters + @declared_attr + def id(cls): + return Column(UUID(as_uuid=True), ForeignKey(Proof.id), primary_key=True) + + +class Proof(Thing): + """Proof over an action. + + """ + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + type = Column(Unicode, nullable=False) + ethereum_hashes = Column(CIText(), default='', nullable=False) + + @property + def url(self) -> urlutils.URL: + """The URL where to GET this proof.""" + return urlutils.URL(url_for_resource(Proof, item_id=self.id)) + + @property + def certificate(self) -> Optional[urlutils.URL]: + return None + + # noinspection PyMethodParameters + @declared_attr + def __mapper_args__(cls): + """Defines inheritance. + + From `the guide `_ + """ + args = {POLYMORPHIC_ID: cls.t} + if cls.t == 'Proof': + args[POLYMORPHIC_ON] = cls.type + # noinspection PyUnresolvedReferences + if JoinedTableMixin in cls.mro(): + args[INHERIT_COND] = cls.id == Proof.id + return args + + def __init__(self, **kwargs) -> None: + # sortedset forces us to do this before calling our parent init + super().__init__(**kwargs) + + def __repr__(self): + return '<{0.t} {0.id} >'.format(self) + + +class ProofTransfer(JoinedTableMixin, Proof): + transfer_id = Column(BigInteger, ForeignKey(Action.id), nullable=False) + transfer = relationship(DisposeProduct, + primaryjoin=DisposeProduct.id == transfer_id) + + +class ProofDataWipe(JoinedTableMixin, Proof): + erasure_type = Column(CIText()) + date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + result = db.Column(db.Boolean, default=False, nullable=False) + erasure_id = Column(BigInteger, ForeignKey(Device.id), nullable=False) + erasure = relationship(EraseBasic, + backref=backref('proofs_datawipe', + lazy=True, + cascade=CASCADE_OWN), + primaryjoin=EraseBasic.id == erasure_id) + + +class ProofFunction(JoinedTableMixin, Proof): + disk_usage = db.Column(db.Integer, default=0) + rate_id = Column(BigInteger, ForeignKey(Rate.id), nullable=False) + rate = relationship(Rate, + primaryjoin=Rate.id == rate_id) + + +class ProofReuse(JoinedTableMixin, Proof): + price = db.Column(db.Integer, required=True) + + +class ProofRecycling(JoinedTableMixin, Proof): + collection_point = Column(CIText()) + date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + contact = Column(CIText()) + ticket = Column(CIText()) + gps_location = Column(CIText()) diff --git a/ereuse_devicehub/resources/proof/schemas.py b/ereuse_devicehub/resources/proof/schemas.py new file mode 100644 index 00000000..73791cc1 --- /dev/null +++ b/ereuse_devicehub/resources/proof/schemas.py @@ -0,0 +1,461 @@ +from flask import current_app as app +from marshmallow import Schema as MarshmallowSchema, ValidationError, fields as f, validates_schema +from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, Nested, String, \ + TimeDelta, UUID +from marshmallow.validate import Length, OneOf, Range +from sqlalchemy.util import OrderedSet +from teal.enums import Country, Currency, Subdivision +from teal.marshmallow import EnumField, IP, SanitizedStr, URL, Version +from teal.resource import Schema + +from ereuse_devicehub.marshmallow import NestedOn +from ereuse_devicehub.resources import enums +from ereuse_devicehub.resources.action import models as m +from ereuse_devicehub.resources.agent import schemas as s_agent +from ereuse_devicehub.resources.device import schemas as s_device +from ereuse_devicehub.resources.enums import AppearanceRange, BiosAccessRange, FunctionalityRange, \ + PhysicalErasureMethod, R_POSITIVE, RatingRange, ReceiverRole, \ + Severity, SnapshotSoftware, TestDataStorageLength +from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE +from ereuse_devicehub.resources.schemas import Thing +from ereuse_devicehub.resources.user import schemas as s_user + + +class Proof(Thing): + __doc__ = m.Action.__doc__ + id = UUID(dump_only=True) + ethereumHashes = SanitizedStr(default='', + validate=Length(max=STR_BIG_SIZE)) + url = URL(dump_only=True, description=m.Action.url.__doc__) + + +class Action(Thing): + __doc__ = m.Action.__doc__ + id = UUID(dump_only=True) + name = SanitizedStr(default='', + validate=Length(max=STR_BIG_SIZE), + description=m.Action.name.comment) + closed = Boolean(missing=True, description=m.Action.closed.comment) + severity = EnumField(Severity, description=m.Action.severity.comment) + description = SanitizedStr(default='', description=m.Action.description.comment) + start_time = DateTime(data_key='startTime', description=m.Action.start_time.comment) + end_time = DateTime(data_key='endTime', description=m.Action.end_time.comment) + snapshot = NestedOn('Snapshot', dump_only=True) + agent = NestedOn(s_agent.Agent, description=m.Action.agent_id.comment) + author = NestedOn(s_user.User, dump_only=True, exclude=('token',)) + components = NestedOn(s_device.Component, dump_only=True, many=True) + parent = NestedOn(s_device.Computer, dump_only=True, description=m.Action.parent_id.comment) + url = URL(dump_only=True, description=m.Action.url.__doc__) + + +class ActionWithOneDevice(Action): + __doc__ = m.ActionWithOneDevice.__doc__ + device = NestedOn(s_device.Device, only_query='id') + + +class ActionWithMultipleDevices(Action): + __doc__ = m.ActionWithMultipleDevices.__doc__ + devices = NestedOn(s_device.Device, + many=True, + required=True, # todo test ensuring len(devices) >= 1 + only_query='id', + collection_class=OrderedSet) + + +class Add(ActionWithOneDevice): + __doc__ = m.Add.__doc__ + + +class Remove(ActionWithOneDevice): + __doc__ = m.Remove.__doc__ + + +class Allocate(ActionWithMultipleDevices): + __doc__ = m.Allocate.__doc__ + to = NestedOn(s_user.User, + description='The user the devices are allocated to.') + organization = SanitizedStr(validate=Length(max=STR_SIZE), + description='The organization where the ' + 'user was when this happened.') + + +class Deallocate(ActionWithMultipleDevices): + __doc__ = m.Deallocate.__doc__ + from_rel = Nested(s_user.User, + data_key='from', + description='The user where the devices are not allocated to anymore.') + organization = SanitizedStr(validate=Length(max=STR_SIZE), + description='The organization where the ' + 'user was when this happened.') + + +class EraseBasic(ActionWithOneDevice): + __doc__ = m.EraseBasic.__doc__ + steps = NestedOn('Step', many=True) + standards = f.List(EnumField(enums.ErasureStandards), dump_only=True) + certificate = URL(dump_only=True) + + +class EraseSectors(EraseBasic): + __doc__ = m.EraseSectors.__doc__ + + +class ErasePhysical(EraseBasic): + __doc__ = m.ErasePhysical.__doc__ + method = EnumField(PhysicalErasureMethod, description=PhysicalErasureMethod.__doc__) + + +class Step(Schema): + __doc__ = m.Step.__doc__ + type = String(description='Only required when it is nested.') + start_time = DateTime(required=True, data_key='startTime') + end_time = DateTime(required=True, data_key='endTime') + severity = EnumField(Severity, description=m.Action.severity.comment) + + +class StepZero(Step): + __doc__ = m.StepZero.__doc__ + + +class StepRandom(Step): + __doc__ = m.StepRandom.__doc__ + + +class Benchmark(ActionWithOneDevice): + __doc__ = m.Benchmark.__doc__ + elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) + + +class BenchmarkDataStorage(Benchmark): + __doc__ = m.BenchmarkDataStorage.__doc__ + read_speed = Float(required=True, data_key='readSpeed') + write_speed = Float(required=True, data_key='writeSpeed') + + +class BenchmarkWithRate(Benchmark): + __doc__ = m.BenchmarkWithRate.__doc__ + rate = Float(required=True) + + +class BenchmarkProcessor(BenchmarkWithRate): + __doc__ = m.BenchmarkProcessor.__doc__ + + +class BenchmarkProcessorSysbench(BenchmarkProcessor): + __doc__ = m.BenchmarkProcessorSysbench.__doc__ + + +class BenchmarkRamSysbench(BenchmarkWithRate): + __doc__ = m.BenchmarkRamSysbench.__doc__ + + +class BenchmarkGraphicCard(BenchmarkWithRate): + __doc__ = m.BenchmarkGraphicCard.__doc__ + + +class Test(ActionWithOneDevice): + __doc__ = m.Test.__doc__ + + +class MeasureBattery(Test): + __doc__ = m.MeasureBattery.__doc__ + size = Integer(required=True, description=m.MeasureBattery.size.comment) + voltage = Integer(required=True, description=m.MeasureBattery.voltage.comment) + cycle_count = Integer(data_key='cycleCount', description=m.MeasureBattery.cycle_count.comment) + health = EnumField(enums.BatteryHealth, description=m.MeasureBattery.health.comment) + + +class TestDataStorage(Test): + __doc__ = m.TestDataStorage.__doc__ + elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) + length = EnumField(TestDataStorageLength, required=True) + status = SanitizedStr(lower=True, validate=Length(max=STR_SIZE), required=True) + lifetime = TimeDelta(precision=TimeDelta.HOURS) + assessment = Boolean() + reallocated_sector_count = Integer(data_key='reallocatedSectorCount') + power_cycle_count = Integer(data_key='powerCycleCount') + reported_uncorrectable_errors = Integer(data_key='reportedUncorrectableErrors') + command_timeout = Integer(data_key='commandTimeout') + current_pending_sector_count = Integer(data_key='currentPendingSectorCount') + offline_uncorrectable = Integer(data_key='offlineUncorrectable') + remaining_lifetime_percentage = Integer(data_key='remainingLifetimePercentage') + + +class StressTest(Test): + __doc__ = m.StressTest.__doc__ + elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) + + +class TestAudio(Test): + __doc__ = m.TestAudio.__doc__ + speaker = Boolean(description=m.TestAudio._speaker.comment) + microphone = Boolean(description=m.TestAudio._microphone.comment) + + +class TestConnectivity(Test): + __doc__ = m.TestConnectivity.__doc__ + + +class TestCamera(Test): + __doc__ = m.TestCamera.__doc__ + + +class TestKeyboard(Test): + __doc__ = m.TestKeyboard.__doc__ + + +class TestTrackpad(Test): + __doc__ = m.TestTrackpad.__doc__ + + +class TestBios(Test): + __doc__ = m.TestBios.__doc__ + bios_power_on = Boolean() + access_range = EnumField(BiosAccessRange, data_key='accessRange') + + +class VisualTest(Test): + __doc__ = m.VisualTest.__doc__ + appearance_range = EnumField(AppearanceRange, required=True, data_key='appearanceRange') + functionality_range = EnumField(FunctionalityRange, + required=True, + data_key='functionalityRange') + labelling = Boolean() + + +class Rate(ActionWithOneDevice): + __doc__ = m.Rate.__doc__ + rating = Integer(validate=Range(*R_POSITIVE), + dump_only=True, + description=m.Rate._rating.comment) + version = Version(dump_only=True, + description=m.Rate.version.comment) + appearance = Integer(validate=Range(enums.R_NEGATIVE), + dump_only=True, + description=m.Rate._appearance.comment) + functionality = Integer(validate=Range(enums.R_NEGATIVE), + dump_only=True, + description=m.Rate._functionality.comment) + rating_range = EnumField(RatingRange, + dump_only=True, + data_key='ratingRange', + description=m.Rate.rating_range.__doc__) + + +class RateComputer(Rate): + __doc__ = m.RateComputer.__doc__ + processor = Float(dump_only=True) + ram = Float(dump_only=True) + data_storage = Float(dump_only=True, data_key='dataStorage') + graphic_card = Float(dump_only=True, data_key='graphicCard') + + data_storage_range = EnumField(RatingRange, dump_only=True, data_key='dataStorageRange') + ram_range = EnumField(RatingRange, dump_only=True, data_key='ramRange') + processor_range = EnumField(RatingRange, dump_only=True, data_key='processorRange') + graphic_card_range = EnumField(RatingRange, dump_only=True, data_key='graphicCardRange') + + +class Price(ActionWithOneDevice): + __doc__ = m.Price.__doc__ + 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) + version = Version(dump_only=True, description=m.Price.version.comment) + rating = NestedOn(Rate, dump_only=True, description=m.Price.rating_id.comment) + + +class EreusePrice(Price): + __doc__ = m.EreusePrice.__doc__ + + class Service(MarshmallowSchema): + class Type(MarshmallowSchema): + amount = Float() + percentage = Float() + + standard = Nested(Type) + warranty2 = Nested(Type) + + warranty2 = Float() + refurbisher = Nested(Service) + retailer = Nested(Service) + platform = Nested(Service) + + +class Install(ActionWithOneDevice): + __doc__ = m.Install.__doc__ + name = SanitizedStr(validate=Length(min=4, max=STR_BIG_SIZE), + required=True, + description='The name of the OS installed.') + elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) + address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256})) + + +class Snapshot(ActionWithOneDevice): + __doc__ = m.Snapshot.__doc__ + """ + The Snapshot updates the state of the device with information about + its components and actions performed at them. + + See docs for more info. + """ + uuid = UUID() + software = EnumField(SnapshotSoftware, + required=True, + description='The software that generated this Snapshot.') + version = Version(required=True, description='The version of the software.') + actions = NestedOn(Action, many=True, dump_only=True) + elapsed = TimeDelta(precision=TimeDelta.SECONDS) + components = NestedOn(s_device.Component, + many=True, + description='A list of components that are inside of the device' + 'at the moment of this Snapshot.' + 'Order is preserved, so the component num 0 when' + 'submitting is the component num 0 when returning it back.') + + @validates_schema + def validate_workbench_version(self, data: dict): + if data['software'] == SnapshotSoftware.Workbench: + if data['version'] < app.config['MIN_WORKBENCH']: + raise ValidationError( + 'Min. supported Workbench version is ' + '{} but yours is {}.'.format(app.config['MIN_WORKBENCH'], data['version']), + field_names=['version'] + ) + + @validates_schema + def validate_components_only_workbench(self, data: dict): + if data['software'] != SnapshotSoftware.Workbench: + if data.get('components', None) is not None: + raise ValidationError('Only Workbench can add component info', + field_names=['components']) + + @validates_schema + def validate_only_workbench_fields(self, data: dict): + """Ensures workbench has ``elapsed`` and ``uuid`` and no others.""" + # todo test + if data['software'] == SnapshotSoftware.Workbench: + if not data.get('uuid', None): + raise ValidationError('Snapshots from Workbench must have uuid', + field_names=['uuid']) + if data.get('elapsed', None) is None: + raise ValidationError('Snapshots from Workbench must have elapsed', + field_names=['elapsed']) + else: + if data.get('uuid', None): + raise ValidationError('Only Snapshots from Workbench can have uuid', + field_names=['uuid']) + if data.get('elapsed', None): + raise ValidationError('Only Snapshots from Workbench can have elapsed', + field_names=['elapsed']) + + +class ToRepair(ActionWithMultipleDevices): + __doc__ = m.ToRepair.__doc__ + + +class Repair(ActionWithMultipleDevices): + __doc__ = m.Repair.__doc__ + + +class Ready(ActionWithMultipleDevices): + __doc__ = m.Ready.__doc__ + + +class ToPrepare(ActionWithMultipleDevices): + __doc__ = m.ToPrepare.__doc__ + + +class Prepare(ActionWithMultipleDevices): + __doc__ = m.Prepare.__doc__ + + +class Live(ActionWithOneDevice): + __doc__ = m.Live.__doc__ + ip = IP(dump_only=True) + subdivision_confidence = Integer(dump_only=True, data_key='subdivisionConfidence') + subdivision = EnumField(Subdivision, dump_only=True) + country = EnumField(Country, dump_only=True) + city = SanitizedStr(lower=True, dump_only=True) + city_confidence = Integer(dump_only=True, data_key='cityConfidence') + isp = SanitizedStr(lower=True, dump_only=True) + organization = SanitizedStr(lower=True, dump_only=True) + organization_type = SanitizedStr(lower=True, dump_only=True, data_key='organizationType') + + +class Organize(ActionWithMultipleDevices): + __doc__ = m.Organize.__doc__ + + +class Reserve(Organize): + __doc__ = m.Reserve.__doc__ + + +class CancelReservation(Organize): + __doc__ = m.CancelReservation.__doc__ + + +class Trade(ActionWithMultipleDevices): + __doc__ = m.Trade.__doc__ + shipping_date = DateTime(data_key='shippingDate') + invoice_number = SanitizedStr(validate=Length(max=STR_SIZE), data_key='invoiceNumber') + price = NestedOn(Price) + to = NestedOn(s_agent.Agent, only_query='id', required=True, comment=m.Trade.to_comment) + confirms = NestedOn(Organize) + + +class Sell(Trade): + __doc__ = m.Sell.__doc__ + + +class Donate(Trade): + __doc__ = m.Donate.__doc__ + + +class Rent(Trade): + __doc__ = m.Rent.__doc__ + + +class MakeAvailable(ActionWithMultipleDevices): + __doc__ = m.MakeAvailable.__doc__ + + +class CancelTrade(Trade): + __doc__ = m.CancelTrade.__doc__ + + +class ToDisposeProduct(Trade): + __doc__ = m.ToDisposeProduct.__doc__ + + +class DisposeProduct(Trade): + __doc__ = m.DisposeProduct.__doc__ + + +class TransferOwnershipBlockchain(Trade): + __doc__ = m.TransferOwnershipBlockchain.__doc__ + + +class Receive(ActionWithMultipleDevices): + __doc__ = m.Receive.__doc__ + role = EnumField(ReceiverRole) + + +class Migrate(ActionWithMultipleDevices): + __doc__ = m.Migrate.__doc__ + other = URL() + + +class MigrateTo(Migrate): + __doc__ = m.MigrateTo.__doc__ + + +class MigrateFrom(Migrate): + __doc__ = m.MigrateFrom.__doc__ + + +class Transferred(ActionWithMultipleDevices): + __doc__ = m.Transferred.__doc__ + + diff --git a/ereuse_devicehub/resources/proof/views.py b/ereuse_devicehub/resources/proof/views.py new file mode 100644 index 00000000..2d127666 --- /dev/null +++ b/ereuse_devicehub/resources/proof/views.py @@ -0,0 +1,43 @@ +from distutils.version import StrictVersion +from typing import List +from uuid import UUID + +from flask import current_app as app, request +from sqlalchemy.util import OrderedSet +from teal.marshmallow import ValidationError +from teal.resource import View + +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.action.models import Action, RateComputer, Snapshot, VisualTest +from ereuse_devicehub.resources.action.rate.v1_0 import CannotRate +from ereuse_devicehub.resources.device.models import Component, Computer +from ereuse_devicehub.resources.enums import SnapshotSoftware + +SUPPORTED_WORKBENCH = StrictVersion('11.0') + + +class ProofView(View): + def post(self): + """Posts batches of proofs.""" + json = request.get_json(validate=False) + if not json: + raise ValidationError('JSON is not correct.') + # todo there should be a way to better get subclassess resource + # defs + if json['batch']: + for proof in json['proofs']: + resource_def = app.resources[proof['type']] + a = resource_def.schema.load(json) + if json['type'] == Snapshot.t: + return self.snapshot(a, resource_def) + if json['type'] == VisualTest.t: + pass + # TODO JN add compute rate with new visual test and old components device + Model = db.Model._decl_class_registry.data[json['type']]() + action = Model(**a) + db.session.add(action) + db.session().final_flush() + ret = self.schema.jsonify(action) + ret.status_code = 201 + db.session.commit() + return ret