diff --git a/ereuse_devicehub/migrations/versions/3ac2bc1897ce_adding_weight_to_tradedocuments.py b/ereuse_devicehub/migrations/versions/3ac2bc1897ce_adding_weight_to_tradedocuments.py new file mode 100644 index 00000000..19f4571c --- /dev/null +++ b/ereuse_devicehub/migrations/versions/3ac2bc1897ce_adding_weight_to_tradedocuments.py @@ -0,0 +1,47 @@ +"""adding weight to tradedocuments + +Revision ID: 3ac2bc1897ce +Revises: 3a3601ac8224 +Create Date: 2021-08-03 16:28:38.719686 + +""" +from alembic import op +import sqlalchemy as sa +from alembic import context +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '3ac2bc1897ce' +down_revision = '7ecb8ff7abad' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + + +def upgrade(): + op.add_column("trade_document", sa.Column("weight", sa.Float(decimal_return_scale=2), nullable=True), schema=f'{get_inv()}') + + # DataWipeDocument table + op.create_table('move_on_document', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("weight", sa.Float(decimal_return_scale=2), nullable=True), + sa.Column('container_from_id', sa.BigInteger(), nullable=False), + sa.Column('container_to_id', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint(['container_from_id'], [f'{get_inv()}.trade_document.id'], ), + sa.ForeignKeyConstraint(['container_to_id'], [f'{get_inv()}.trade_document.id'], ), + sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}' + ) + + +def downgrade(): + op.drop_column('trade_document', 'weight', schema=f'{get_inv()}') + op.drop_table('move_on_document', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/resources/action/__init__.py b/ereuse_devicehub/resources/action/__init__.py index bdc55014..0c3d3e9c 100644 --- a/ereuse_devicehub/resources/action/__init__.py +++ b/ereuse_devicehub/resources/action/__init__.py @@ -313,3 +313,8 @@ class MigrateToDef(ActionDef): class MigrateFromDef(ActionDef): VIEW = None SCHEMA = schemas.MigrateFrom + + +class MoveOnDocumentDef(ActionDef): + VIEW = None + SCHEMA = schemas.MoveOnDocument diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index fdbdf1fa..603462b8 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -62,6 +62,15 @@ _sorted_actions = { 'order_by': lambda: Action.end_time, 'collection_class': SortedSet } + + +def sorted_actions_by(data): + return { + 'order_by': lambda: data, + 'collection_class': SortedSet + } + + """For db.backref, return the actions sorted by end_time.""" @@ -1642,6 +1651,35 @@ class MakeAvailable(ActionWithMultipleDevices): pass +class MoveOnDocument(JoinedTableMixin, ActionWithMultipleTradeDocuments): + """Action than certify one movement of some indescriptible material of + one container to an other.""" + + weight = db.Column(db.Float(nullable=True)) + weight.comment = """Weight than go to recycling""" + container_from_id = db.Column( + db.BigInteger, + db.ForeignKey('trade_document.id'), + nullable=False + ) + container_from = db.relationship( + 'TradeDocument', + primaryjoin='MoveOnDocument.container_from_id == TradeDocument.id', + ) + container_from_id.comment = """This is the trade document used as container in a incoming lot""" + + container_to_id = db.Column( + db.BigInteger, + db.ForeignKey('trade_document.id'), + nullable=False + ) + container_to = db.relationship( + 'TradeDocument', + primaryjoin='MoveOnDocument.container_to_id == TradeDocument.id', + ) + container_to_id.comment = """This is the trade document used as container in a outgoing lot""" + + class Migrate(JoinedTableMixin, ActionWithMultipleDevices): """Moves the devices to a new database/inventory. Devices cannot be modified anymore at the previous database. diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index 3232f11b..ec11dd8f 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -2,7 +2,7 @@ import copy from datetime import datetime, timedelta from dateutil.tz import tzutc from flask import current_app as app, g -from marshmallow import Schema as MarshmallowSchema, ValidationError, fields as f, validates_schema +from marshmallow import Schema as MarshmallowSchema, ValidationError, fields as f, validates_schema, pre_load, post_load from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, Nested, String, \ TimeDelta, UUID from marshmallow.validate import Length, OneOf, Range @@ -25,6 +25,7 @@ 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 from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.resources.tradedocument.models import TradeDocument class Action(Thing): @@ -840,3 +841,31 @@ class MigrateTo(Migrate): class MigrateFrom(Migrate): __doc__ = m.MigrateFrom.__doc__ + + +class MoveOnDocument(Action): + __doc__ = m.MoveOnDocument.__doc__ + weight = Integer() + container_from = NestedOn('TradeDocument', only_query='id') + container_to = NestedOn('TradeDocument', only_query='id') + + @pre_load + def extract_container(self, data): + id_hash = data['container_to'] + docs = TradeDocument.query.filter_by(owner=g.user, file_hash=id_hash).all() + if len(docs) > 1: + txt = 'This document it is associated in more than one lot' + raise ValidationError(txt) + + if len(docs) < 1: + txt = 'This document not exist' + raise ValidationError(txt) + data['container_to'] = docs[0].id + + @post_load + def adding_documents(self, data): + """Adding action in the 2 TradeDocuments""" + docs = OrderedSet() + docs.add(data['container_to']) + docs.add(data['container_from']) + data['documents'] = docs diff --git a/ereuse_devicehub/resources/action/views/documents.py b/ereuse_devicehub/resources/action/views/documents.py index e1f685b5..d099cba4 100644 --- a/ereuse_devicehub/resources/action/views/documents.py +++ b/ereuse_devicehub/resources/action/views/documents.py @@ -3,7 +3,7 @@ import copy from ereuse_devicehub.db import db from ereuse_devicehub.resources.action.models import DataWipe from ereuse_devicehub.resources.documents.models import DataWipeDocument -from ereuse_devicehub.resources.device.models import DataStorage +from ereuse_devicehub.resources.device.models import DataStorage from ereuse_devicehub.resources.documents.schemas import DataWipeDocument as sh_document from ereuse_devicehub.resources.hash_reports import ReportHash @@ -31,14 +31,14 @@ class ErasedView(): doc_data = schema.load(data) self.document = DataWipeDocument(**doc_data) db.session.add(self.document) - + db_hash = ReportHash(hash3=self.document.file_hash) db.session.add(db_hash) def insert_action(self, data): [data.pop(x, None) for x in ['url', 'documentId', 'filename', 'hash', 'software', 'success']] self.data = self.schema.load(data) - + for dev in self.data['devices']: if not hasattr(dev, 'components'): continue diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index 98cdf956..f6de8884 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -434,3 +434,4 @@ class DocumentDef(Resource): auth=app.auth) wbconf_view = app.auth.requires_auth(wbconf_view) self.add_url_rule('/wbconf/', view_func=wbconf_view, methods=get) + diff --git a/ereuse_devicehub/resources/documents/models.py b/ereuse_devicehub/resources/documents/models.py index a2b98511..9c557b3e 100644 --- a/ereuse_devicehub/resources/documents/models.py +++ b/ereuse_devicehub/resources/documents/models.py @@ -1,14 +1,23 @@ -from citext import CIText from flask import g +from citext import CIText +from sortedcontainers import SortedSet from sqlalchemy import BigInteger, Column, Sequence, Unicode, Boolean, ForeignKey from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.dialects.postgresql import UUID -from teal.db import URL +from sqlalchemy.orm import backref +from teal.db import CASCADE_OWN, URL + from ereuse_devicehub.db import db from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.models import Thing, STR_SM_SIZE +_sorted_documents = { + 'order_by': lambda: Document.created, + 'collection_class': SortedSet +} + + class Document(Thing): """This represent a generic document.""" diff --git a/ereuse_devicehub/resources/documents/schemas.py b/ereuse_devicehub/resources/documents/schemas.py index 6500777a..d62cec41 100644 --- a/ereuse_devicehub/resources/documents/schemas.py +++ b/ereuse_devicehub/resources/documents/schemas.py @@ -1,7 +1,10 @@ -from marshmallow.fields import DateTime, Integer, validate, Boolean +from marshmallow.fields import DateTime, Integer, validate, Boolean, Float from marshmallow import post_load +from marshmallow.validate import Range from teal.marshmallow import SanitizedStr, URL +from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.schemas import Thing +from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.documents import models as m diff --git a/ereuse_devicehub/resources/tradedocument/models.py b/ereuse_devicehub/resources/tradedocument/models.py index 9c743244..36ae370a 100644 --- a/ereuse_devicehub/resources/tradedocument/models.py +++ b/ereuse_devicehub/resources/tradedocument/models.py @@ -1,4 +1,3 @@ -import copy from citext import CIText from flask import g @@ -71,6 +70,8 @@ class TradeDocument(Thing): file_hash.comment = """This is the hash of the file produced from frontend.""" url = db.Column(URL()) url.comment = """This is the url where resides the document.""" + weight = db.Column(db.Float(nullable=True)) + weight.comment = """This is the weight of one container than this document express.""" __table_args__ = ( db.Index('document_id', id, postgresql_using='hash'), @@ -100,9 +101,8 @@ class TradeDocument(Thing): revoke = 'Revoke' revoke_pending = 'Revoke Pending' confirm_revoke = 'Document Revoked' - if not self.actions: - return + return ac = self.actions[-1] @@ -110,7 +110,7 @@ class TradeDocument(Thing): # can to do revoke_confirmed return confirm_revoke - if ac.type == 'RevokeDocument': + if ac.type == 'RevokeDocument': if ac.user == g.user: # can todo revoke_pending return revoke_pending @@ -130,9 +130,22 @@ class TradeDocument(Thing): # can to do revoke return double_confirm - def _warning_actions(self, actions): - return sorted(ev for ev in actions if ev.severity >= Severity.Warning) + @property + def total_weight(self): + """Return all weight than this container have.""" + weight = self.weight or 0 + for x in self.actions: + if not x.type == 'MoveOnDocument' or not x.weight: + continue + if self == x.container_from: + continue + weight += x.weight + return weight + + def _warning_actions(self, actions): + """Show warning actions""" + return sorted(ev for ev in actions if ev.severity >= Severity.Warning) def __lt__(self, other): return self.id < other.id diff --git a/ereuse_devicehub/resources/tradedocument/schemas.py b/ereuse_devicehub/resources/tradedocument/schemas.py index fab95dc4..99aa9ab6 100644 --- a/ereuse_devicehub/resources/tradedocument/schemas.py +++ b/ereuse_devicehub/resources/tradedocument/schemas.py @@ -1,4 +1,4 @@ -from marshmallow.fields import DateTime, Integer, validate +from marshmallow.fields import DateTime, Integer, Float, validate from teal.marshmallow import SanitizedStr, URL # from marshmallow import ValidationError, validates_schema @@ -29,3 +29,5 @@ class TradeDocument(Thing): url = URL(description=m.TradeDocument.url.comment) lot = NestedOn('Lot', only_query='id', description=m.TradeDocument.lot.__doc__) trading = SanitizedStr(dump_only=True, description='') + weight = Float(required=False, description=m.TradeDocument.weight.comment) + total_weight = Float(required=False, description=m.TradeDocument.weight.comment) diff --git a/tests/test_action.py b/tests/test_action.py index 03868c3c..4d2520c0 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -28,6 +28,7 @@ from ereuse_devicehub.resources.action import models from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \ RamModule, SolidStateDrive +from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.enums import ComputerChassis, Severity, TestDataStorageLength from tests import conftest from tests.conftest import create_user, file, yaml2json, json_encode @@ -2438,3 +2439,64 @@ def test_action_web_erase(user: UserClient, client: Client): assert "alert alert-info" in response assert "100% coincidence." in response assert not "alert alert-danger" in response + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_moveOnDocument(user: UserClient, user2: UserClient): + lotIn, _ = user.post({'name': 'MyLotIn'}, res=Lot) + lotOut, _ = user.post({'name': 'MyLotOut'}, res=Lot) + url = 'http://www.ereuse.org/apapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapaaaa', + request_post1 = { + 'filename': 'test.pdf', + 'hash': 'bbbbbbbb', + 'url': url, + 'weight': 150, + 'lot': lotIn['id'] + } + tradedocument_from, _ = user.post(res=TradeDocument, data=request_post1) + id_hash = 'aaaaaaaaa' + request_post2 = { + 'filename': 'test.pdf', + 'hash': id_hash, + 'url': url, + 'weight': 0, + 'lot': lotOut['id'] + } + tradedocument_to, _ = user.post(res=TradeDocument, data=request_post2) + + request_trade = { + 'type': 'Trade', + 'devices': [], + 'userFromEmail': user2.email, + 'userToEmail': user.email, + 'price': 10, + 'date': "2020-12-01T02:00:00+00:00", + 'lot': lotIn['id'], + 'confirms': True, + } + + user.post(res=models.Action, data=request_trade) + + description = 'This is a good description' + request_moveOn = { + 'type': 'MoveOnDocument', + 'weight': 15, + 'container_from': tradedocument_from['id'], + 'container_to': id_hash, + 'description': description + } + doc, _ = user.post(res=models.Action, data=request_moveOn) + + assert doc['weight'] == request_moveOn['weight'] + assert doc['container_from']['id'] == tradedocument_from['id'] + assert doc['container_to']['id'] == tradedocument_to['id'] + + mvs= models.MoveOnDocument.query.filter().first() + trade_from = TradeDocument.query.filter_by(id=tradedocument_from['id']).one() + trade_to = TradeDocument.query.filter_by(id=tradedocument_to['id']).one() + assert trade_from in mvs.documents + assert trade_to in mvs.documents + assert description == mvs.description + tradedocument_to, _ = user.post(res=TradeDocument, data=request_post2) + user.post(res=models.Action, data=request_moveOn, status=422) diff --git a/tests/test_basic.py b/tests/test_basic.py index c04e8fb6..c3dc44d3 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -122,4 +122,4 @@ def test_api_docs(client: Client): 'scheme': 'basic', 'name': 'Authorization' } - assert len(docs['definitions']) == 126 + assert len(docs['definitions']) == 127 diff --git a/tests/test_documents.py b/tests/test_documents.py index 28965c77..240fa738 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -16,6 +16,7 @@ from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.user.models import Session from ereuse_devicehub.resources.action.models import Snapshot, Allocate, Live from ereuse_devicehub.resources.documents import documents +from ereuse_devicehub.resources.tradedocument.models import TradeDocument from ereuse_devicehub.resources.device import models as d from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.tag.model import Tag @@ -677,3 +678,41 @@ def test_get_wbconf(user: UserClient): assert token in env user.user['token'] = token snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot) + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_trade_documents(user: UserClient): + """Tests upload one document""" + + lot, _ = user.post({'name': 'MyLot'}, res=Lot) + request_post = { + 'filename': 'test.pdf', + 'hash': 'bbbbbbbb', + 'url': 'http://www.ereuse.org/', + 'lot': lot['id'] + } + doc, _ = user.post(res=TradeDocument, data=request_post) + assert doc['filename'] == request_post['filename'] + assert doc['url'] == request_post['url'] + assert doc['hash'] == request_post['hash'] + assert doc['lot']['name'] == lot['name'] + + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_trade_documents_with_weight(user: UserClient): + """Tests upload one document""" + + lot, _ = user.post({'name': 'MyLot'}, res=Lot) + # long url + url = 'http://www.ereuse.org/apapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapapaapaapaapaapaapaapaapaapaapaaaa', + request_post = { + 'filename': 'test.pdf', + 'hash': 'bbbbbbbb', + 'url': url, + 'weight': 15, + 'lot': lot['id'] + } + doc, _ = user.post(res=TradeDocument, data=request_post) + assert doc['weight'] == request_post['weight']