diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index be15650d..b1db6e9e 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -7,7 +7,8 @@ from teal.config import Config from teal.enums import Currency from teal.utils import import_resource -from ereuse_devicehub.resources import action, agent, deliverynote, inventory, lot, tag, user +from ereuse_devicehub.resources import action, agent, deliverynote, inventory, \ + lot, proof, tag, user from ereuse_devicehub.resources.device import definitions from ereuse_devicehub.resources.documents import documents from ereuse_devicehub.resources.enums import PriceSoftware @@ -21,6 +22,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/deliverynote/models.py b/ereuse_devicehub/resources/deliverynote/models.py index 7396977e..9e2bb7d3 100644 --- a/ereuse_devicehub/resources/deliverynote/models.py +++ b/ereuse_devicehub/resources/deliverynote/models.py @@ -4,8 +4,9 @@ from datetime import datetime from boltons import urlutils from citext import CIText from flask import g +from typing import Iterable from sqlalchemy.types import ARRAY -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import UUID, JSONB from teal.db import CASCADE_OWN, check_range, IntEnum from teal.resource import url_for_resource @@ -41,7 +42,8 @@ class Deliverynote(Thing): # to SnapshotDelivery entity. # At this stage of implementation they will treated as a # comma-separated string of the devices expexted/transfered - expected_devices = db.Column(db.ARRAY(db.Integer, dimensions=1), nullable=False) + expected_devices = db.Column(JSONB, nullable=False) + # expected_devices = db.Column(db.ARRAY(JSONB, dimensions=1), nullable=False) transferred_devices = db.Column(db.ARRAY(db.Integer, dimensions=1), nullable=True) transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False) transfer_state.comment = TransferState.__doc__ @@ -55,8 +57,8 @@ class Deliverynote(Thing): primaryjoin=Lot.id == lot_id) def __init__(self, document_id: str, deposit: str, date, - supplier_email: str, - expected_devices: str, + supplier_email: str, + expected_devices: Iterable, transfer_state: TransferState) -> None: """Initializes a delivery note """ diff --git a/ereuse_devicehub/resources/deliverynote/schemas.py b/ereuse_devicehub/resources/deliverynote/schemas.py index 3e649b4e..3028c30f 100644 --- a/ereuse_devicehub/resources/deliverynote/schemas.py +++ b/ereuse_devicehub/resources/deliverynote/schemas.py @@ -22,8 +22,7 @@ class Deliverynote(Thing): date = f.DateTime('iso', required=True) deposit = f.Integer(validate=f.validate.Range(min=0, max=100), description=m.Deliverynote.deposit.__doc__) - ethereum_address = f.String(description='User identifier address inside the Blockchain', - data_key='ethereumAddress') - expected_devices = f.List(f.Integer(), required=True, data_key='expectedDevices') + ethereum_address = f.String(description='User identifier address inside the Blockchain') + expected_devices = f.List(f.Dict, required=True, data_key='expectedDevices') transferred_devices = f.List(f.Integer(), required=False, data_key='transferredDevices') transfer_state = EnumField(TransferState, description=m.Lot.transfer_state.comment) diff --git a/ereuse_devicehub/resources/proof/__init__.py b/ereuse_devicehub/resources/proof/__init__.py new file mode 100644 index 00000000..ef3a68a6 --- /dev/null +++ b/ereuse_devicehub/resources/proof/__init__.py @@ -0,0 +1,37 @@ +from teal.resource import Converters, Resource + +from ereuse_devicehub.resources.proof import schemas +from ereuse_devicehub.resources.proof.views import ProofView + + +class ProofDef(Resource): + SCHEMA = schemas.Proof + VIEW = ProofView + # AUTH = True + AUTH = False + ID_CONVERTER = Converters.uuid + + +class ProofTransferDef(ProofDef): + VIEW = None + SCHEMA = schemas.ProofTransfer + + +class ProofDataWipeDef(ProofDef): + VIEW = None + SCHEMA = schemas.ProofDataWipe + + +class ProofFunction(ProofDef): + VIEW = None + SCHEMA = schemas.ProofFunction + + +class ProofReuse(ProofDef): + VIEW = None + SCHEMA = schemas.ProofReuse + + +class ProofRecycling(ProofDef): + VIEW = None + SCHEMA = schemas.ProofRecycling diff --git a/ereuse_devicehub/resources/proof/models.py b/ereuse_devicehub/resources/proof/models.py new file mode 100644 index 00000000..60e18ae3 --- /dev/null +++ b/ereuse_devicehub/resources/proof/models.py @@ -0,0 +1,119 @@ +"""This file contains all proofs related to actions + +""" + +from collections import Iterable +from datetime import datetime +from typing import Optional, Set, Union +from uuid import uuid4 + +from boltons import urlutils +from citext import CIText +from flask import current_app as app, g +from sortedcontainers import SortedSet +from sqlalchemy import BigInteger, Column, Enum as DBEnum, \ + ForeignKey, Integer, Unicode +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.util import OrderedSet +from teal.db import CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, \ + POLYMORPHIC_ON, StrictVersionType, URL +from teal.marshmallow import ValidationError +from teal.resource import url_for_resource + +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.action.models import Action, DisposeProduct, \ + EraseBasic, Rate, Trade +from ereuse_devicehub.resources.models import Thing + + +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)) + + # 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(UUID, ForeignKey(Trade.id), nullable=False) + transfer = relationship(DisposeProduct, + backref=backref("proof_transfer", + lazy=True, + cascade=CASCADE_OWN), + uselist=False, + primaryjoin=DisposeProduct.id == transfer_id) + + +class ProofDataWipe(JoinedTableMixin, Proof): + erasure_type = Column(CIText(), default='', nullable=False) + date = Column(db.DateTime, nullable=False, default=datetime.utcnow) + result = Column(db.Boolean, default=False, nullable=False) + result.comment = """Identifies proof datawipe as a result.""" + erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), nullable=False) + erasure = relationship(EraseBasic, + backref=backref('proof_datawipe', + lazy=True, + cascade=CASCADE_OWN), + primaryjoin=EraseBasic.id == erasure_id) + + +class ProofFunction(JoinedTableMixin, Proof): + disk_usage = Column(db.Integer, default=0) + rate_id = Column(UUID, ForeignKey(Rate.id), nullable=False) + rate = relationship(Rate, + backref=backref('proof_function', + lazy=True, + cascade=CASCADE_OWN), + primaryjoin=Rate.id == rate_id) + + +class ProofReuse(JoinedTableMixin, Proof): + price = Column(db.Integer) + + +class ProofRecycling(JoinedTableMixin, Proof): + collection_point = Column(CIText(), default='', nullable=False) + date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + contact = Column(CIText(), default='', nullable=False) + ticket = Column(CIText(), default='', nullable=False) + gps_location = Column(CIText(), default='', nullable=False) diff --git a/ereuse_devicehub/resources/proof/schemas.py b/ereuse_devicehub/resources/proof/schemas.py new file mode 100644 index 00000000..4859150a --- /dev/null +++ b/ereuse_devicehub/resources/proof/schemas.py @@ -0,0 +1,56 @@ +from flask import current_app as app +from marshmallow import Schema as MarshmallowSchema, ValidationError, validates_schema +from marshmallow.fields import Boolean, DateTime, Integer, Nested, String, UUID +from marshmallow.validate import Length +from sqlalchemy.util import OrderedSet +from teal.marshmallow import SanitizedStr, URL +from teal.resource import Schema + +from ereuse_devicehub.marshmallow import NestedOn +from ereuse_devicehub.resources.proof import models as m +from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE +from ereuse_devicehub.resources.schemas import Thing +from ereuse_devicehub.resources.action import schemas as s_action + + +class Proof(Thing): + __doc__ = m.Proof.__doc__ + id = UUID(dump_only=True) + ethereum_hashes = SanitizedStr(default='', validate=Length(max=STR_BIG_SIZE), + data_key="ethereumHashes") + url = URL(dump_only=True, description=m.Proof.url.__doc__) + + +class ProofTransfer(Proof): + __doc__ = m.ProofTransfer.__doc__ + transfer = NestedOn(s_action.DisposeProduct, + required=True, + only_query='id') + + +class ProofDataWipe(Proof): + __doc__ = m.ProofDataWipe.__doc__ + erasure_type = SanitizedStr(default='') + date = DateTime('iso', required=True) + result = Boolean(missing=False) + erasure = NestedOn(s_action.EraseBasic, only_query='id') + + +class ProofFunction(Proof): + __doc__ = m.ProofFunction.__doc__ + disk_usage = Integer() + rate = NestedOn(s_action.Rate, required=True, only_query='id') + + +class ProofReuse(Proof): + __doc__ = m.ProofReuse.__doc__ + price = Integer() + + +class ProofRecycling(Proof): + __doc__ = m.ProofRecycling.__doc__ + collection_point = SanitizedStr(default='') + date = DateTime() + contact = SanitizedStr(default='') + ticket = SanitizedStr(default='') + gps_location = SanitizedStr(default='') diff --git a/ereuse_devicehub/resources/proof/views.py b/ereuse_devicehub/resources/proof/views.py new file mode 100644 index 00000000..c645479d --- /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, jsonify +from sqlalchemy.util import OrderedSet +from teal.marshmallow import ValidationError +from teal.resource import View + +from ereuse_devicehub.db import db +from ereuse_devicehub.query import things_response +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 + proofs = list() + if json['batch']: + for prf in json['proofs']: + resource_def = app.resources[prf['type']] + p = resource_def.schema.load(prf) + Model = db.Model._decl_class_registry.data[prf['type']]() + proof = Model(**p) + db.session.add(proof) + proofs.append(self.schema.dump(proof)) + db.session.commit() + response = jsonify({ + 'items': proofs, + 'url': request.path + }) + response.status_code = 201 + return response