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/views.py b/ereuse_devicehub/resources/deliverynote/views.py index 0b0cb35a..02a1ff8b 100644 --- a/ereuse_devicehub/resources/deliverynote/views.py +++ b/ereuse_devicehub/resources/deliverynote/views.py @@ -16,6 +16,7 @@ from ereuse_devicehub.db import db from ereuse_devicehub.query import things_response from ereuse_devicehub.resources.deliverynote.models import Deliverynote from ereuse_devicehub.resources.lot.models import Lot +from ereuse_devicehub.resources.device.models import Computer class DeliverynoteView(View): @@ -41,8 +42,18 @@ class DeliverynoteView(View): 'ethereum_address'), partial=True) d = request.get_json(schema=patch_schema) dlvnote = Deliverynote.query.filter_by(id=id).one() + # device_fields = ['transfer_state', 'deliverynote_address'] + # computers = [x for x in dlvnote.transferred_devices if isinstance(x, Computer)] for key, value in d.items(): setattr(dlvnote, key, value) + # Transalate ethereum_address attribute + # devKey = key + # if key == 'ethereum_address': + # devKey = 'deliverynote_address' + # if devKey in device_fields: + # for dev in computers: + # setattr(dev, devKey, value) + db.session.commit() return Response(status=204) diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 1ecbe8f3..68134b8e 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -382,6 +382,7 @@ class Computer(Device): It is a subset of the Linux definition of DMI / DMI decode. """ + ethereum_address = Column(CIText(), unique=True, default=None) deposit = Column(Integer, check_range('deposit',min=0,max=100), default=0) owner_address = db.Column(CIText(), db.ForeignKey(User.ethereum_address), diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 093cec75..61c399ad 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -122,6 +122,7 @@ class Computer(Device): dump_only=True, collection_class=set, description=m.Computer.privacy.__doc__) + ethereum_address = SanitizedStr(validate=f.validate.Length(max=42)) deposit = Integer(validate=f.validate.Range(min=0, max=100), description=m.Computer.deposit.__doc__) # author_id = NestedOn(s_user.User,only_query='author_id') diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index 665acd68..1aaa1f93 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -1,10 +1,10 @@ import datetime import marshmallow -from flask import current_app as app, render_template, request +from flask import current_app as app, render_template, request, Response from flask.json import jsonify from flask_sqlalchemy import Pagination -from marshmallow import fields, fields as f, validate as v +from marshmallow import fields, fields as f, validate as v, ValidationError from teal import query from teal.cache import cache from teal.resource import View @@ -15,7 +15,7 @@ from ereuse_devicehub.query import SearchQueryParser, things_response from ereuse_devicehub.resources import search from ereuse_devicehub.resources.action import models as actions from ereuse_devicehub.resources.device import states -from ereuse_devicehub.resources.device.models import Device, Manufacturer +from ereuse_devicehub.resources.device.models import Device, Manufacturer, Computer from ereuse_devicehub.resources.device.search import DeviceSearch from ereuse_devicehub.resources.lot.models import LotDeviceDescendants from ereuse_devicehub.resources.tag.model import Tag @@ -92,7 +92,24 @@ class DeviceView(View): description: The device or devices. """ return super().get(id) - + + def patch(self, id): + dev = Device.query.filter_by(id=id).one() + if isinstance(dev, Computer): + resource_def = app.resources['Computer'] + # TODO check how to handle the 'actions_one' + patch_schema = resource_def.SCHEMA(only=['ethereum_address', 'transfer_state', 'deliverynote_address', 'actions_one'], partial=True) + json = request.get_json(schema=patch_schema) + # TODO check how to handle the 'actions_one' + json.pop('actions_one') + if not dev: + raise ValueError('Device non existent') + for key, value in json.items(): + setattr(dev,key,value) + db.session.commit() + return Response(status=204) + raise ValueError('Cannot patch a non computer') + def one(self, id: int): """Gets one device.""" if not request.authorization: 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..2562b446 --- /dev/null +++ b/ereuse_devicehub/resources/proof/models.py @@ -0,0 +1,158 @@ +"""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.device.models import Device +from ereuse_devicehub.resources.models import Thing +from ereuse_devicehub.resources.user 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_hash = Column(CIText(), default='', nullable=False) + device_id = db.Column(BigInteger, + db.ForeignKey(Device.id), + nullable=False) + device = db.relationship(Device, + backref=db.backref('proofs_device', uselist=True, lazy=True), + lazy=True, + primaryjoin=Device.id == device_id) + + @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): + supplier_id = db.Column(UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id) + supplier = db.relationship(User, primaryjoin=lambda: ProofTransfer.supplier_id == User.id) + receiver_id = db.Column(UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False) + receiver = db.relationship(User, primaryjoin=lambda: ProofTransfer.receiver_id == User.id) + deposit = Column(db.Integer, default=0) + + +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.""" + proof_author_id = Column(UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id) + proof_author = relationship(User, primaryjoin=lambda: ProofDataWipe.proof_author_id == User.id) + erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), nullable=False) + erasure = relationship(EraseBasic, + backref=backref('proof_datawipe', + lazy=True, + uselist=False, + cascade=CASCADE_OWN), + primaryjoin=EraseBasic.id == erasure_id) + + +class ProofFunction(JoinedTableMixin, Proof): + disk_usage = Column(db.Integer, default=0) + proof_author_id = Column(UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id) + proof_author = db.relationship(User, primaryjoin=lambda: ProofFunction.proof_author_id == User.id) + rate_id = Column(UUID(as_uuid=True), ForeignKey(Rate.id), nullable=False) + rate = relationship(Rate, + backref=backref('proof_function', + lazy=True, + uselist=False, + cascade=CASCADE_OWN), + primaryjoin=Rate.id == rate_id) + + +class ProofReuse(JoinedTableMixin, Proof): + receiver_segment = Column(CIText(), default='', nullable=False) + id_receipt = Column(CIText(), default='', nullable=False) + supplier_id = db.Column(UUID(as_uuid=True), + db.ForeignKey(User.id), + # nullable=False, + # default=lambda: g.user.id) + nullable=True) + supplier = db.relationship(User, primaryjoin=lambda: ProofReuse.supplier_id == User.id) + receiver_id = db.Column(UUID(as_uuid=True), + db.ForeignKey(User.id), + # nullable=False) + nullable=True) + receiver = db.relationship(User, primaryjoin=lambda: ProofReuse.receiver_id == User.id) + price = Column(db.Integer) + + +class ProofRecycling(JoinedTableMixin, Proof): + collection_point = Column(CIText(), default='', nullable=False) + date = 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) + recycler_code = 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..de18a41f --- /dev/null +++ b/ereuse_devicehub/resources/proof/schemas.py @@ -0,0 +1,72 @@ +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, 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 +from ereuse_devicehub.resources.device import schemas as s_device +from ereuse_devicehub.resources.user import schemas as s_user + + +class Proof(Thing): + __doc__ = m.Proof.__doc__ + id = UUID(dump_only=True) + ethereum_hash = SanitizedStr(default='', validate=Length(max=STR_BIG_SIZE), + data_key="ethereumHash", required=True) + url = URL(dump_only=True, description=m.Proof.url.__doc__) + device_id = Integer(load_only=True, data_key='deviceID') + device = NestedOn(s_device.Device, dump_only=True) + + +class ProofTransfer(Proof): + __doc__ = m.ProofTransfer.__doc__ + deposit = Integer(validate=f.validate.Range(min=0, max=100)) + supplier_id = UUID(load_only=True, required=True, data_key='supplierID') + receiver_id = UUID(load_only=True, required=True, data_key='receiverID') + + +class ProofDataWipe(Proof): + __doc__ = m.ProofDataWipe.__doc__ + # erasure_type = String(default='', data_key='erasureType') + date = DateTime('iso', required=True) + result = Boolean(required=True) + proof_author_id = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), + load_only=True, required=True, data_key='proofAuthorID') + proof_author = NestedOn(s_user.User, dump_only=True) + erasure = NestedOn(s_action.EraseBasic, only_query='id', data_key='erasureID') + + +class ProofFunction(Proof): + __doc__ = m.ProofFunction.__doc__ + disk_usage = Integer(validate=f.validate.Range(min=0, max=100), data_key='diskUsage') + proof_author_id = SanitizedStr(validate=f.validate.Length(max=STR_SIZE), + load_only=True, required=True, data_key='proofAuthorID') + proof_author = NestedOn(s_user.User, dump_only=True) + rate = NestedOn(s_action.Rate, required=True, + only_query='id', data_key='rateID') + + +class ProofReuse(Proof): + __doc__ = m.ProofReuse.__doc__ + receiver_segment = String(default='', data_key='receiverSegment', required=True) + id_receipt = String(default='', data_key='idReceipt', required=True) + supplier_id = UUID(load_only=True, required=False, data_key='supplierID') + receiver_id = UUID(load_only=True, required=False, data_key='receiverID') + price = Integer(required=True) + + +class ProofRecycling(Proof): + __doc__ = m.ProofRecycling.__doc__ + collection_point = SanitizedStr(default='', data_key='collectionPoint', required=True) + date = DateTime('iso', required=True) + contact = SanitizedStr(default='', required=True) + ticket = SanitizedStr(default='', required=True) + gps_location = SanitizedStr(default='', data_key='gpsLocation', required=True) + recycler_code = SanitizedStr(default='', data_key='recyclerCode', required=True) diff --git a/ereuse_devicehub/resources/proof/views.py b/ereuse_devicehub/resources/proof/views.py new file mode 100644 index 00000000..9b016df9 --- /dev/null +++ b/ereuse_devicehub/resources/proof/views.py @@ -0,0 +1,44 @@ +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(resource_def.schema.dump(proof)) + db.session().final_flush() + db.session.commit() + response = jsonify({ + 'items': proofs, + 'url': request.path + }) + response.status_code = 201 + return response