Merge pull request #163 from eReuse/feature/container

Feature/container
This commit is contained in:
cayop 2021-09-29 12:58:01 +02:00 committed by GitHub
commit baa5fccda0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 263 additions and 15 deletions

View File

@ -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()}')

View File

@ -313,3 +313,8 @@ class MigrateToDef(ActionDef):
class MigrateFromDef(ActionDef): class MigrateFromDef(ActionDef):
VIEW = None VIEW = None
SCHEMA = schemas.MigrateFrom SCHEMA = schemas.MigrateFrom
class MoveOnDocumentDef(ActionDef):
VIEW = None
SCHEMA = schemas.MoveOnDocument

View File

@ -62,6 +62,15 @@ _sorted_actions = {
'order_by': lambda: Action.end_time, 'order_by': lambda: Action.end_time,
'collection_class': SortedSet '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.""" """For db.backref, return the actions sorted by end_time."""
@ -1642,6 +1651,35 @@ class MakeAvailable(ActionWithMultipleDevices):
pass 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): class Migrate(JoinedTableMixin, ActionWithMultipleDevices):
"""Moves the devices to a new database/inventory. Devices cannot be """Moves the devices to a new database/inventory. Devices cannot be
modified anymore at the previous database. modified anymore at the previous database.

View File

@ -2,7 +2,7 @@ import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dateutil.tz import tzutc from dateutil.tz import tzutc
from flask import current_app as app, g 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, \ from marshmallow.fields import Boolean, DateTime, Decimal, Float, Integer, Nested, String, \
TimeDelta, UUID TimeDelta, UUID
from marshmallow.validate import Length, OneOf, Range 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.schemas import Thing
from ereuse_devicehub.resources.user import schemas as s_user from ereuse_devicehub.resources.user import schemas as s_user
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
class Action(Thing): class Action(Thing):
@ -840,3 +841,31 @@ class MigrateTo(Migrate):
class MigrateFrom(Migrate): class MigrateFrom(Migrate):
__doc__ = m.MigrateFrom.__doc__ __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

View File

@ -3,7 +3,7 @@ import copy
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import DataWipe from ereuse_devicehub.resources.action.models import DataWipe
from ereuse_devicehub.resources.documents.models import DataWipeDocument 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.documents.schemas import DataWipeDocument as sh_document
from ereuse_devicehub.resources.hash_reports import ReportHash from ereuse_devicehub.resources.hash_reports import ReportHash
@ -31,14 +31,14 @@ class ErasedView():
doc_data = schema.load(data) doc_data = schema.load(data)
self.document = DataWipeDocument(**doc_data) self.document = DataWipeDocument(**doc_data)
db.session.add(self.document) db.session.add(self.document)
db_hash = ReportHash(hash3=self.document.file_hash) db_hash = ReportHash(hash3=self.document.file_hash)
db.session.add(db_hash) db.session.add(db_hash)
def insert_action(self, data): def insert_action(self, data):
[data.pop(x, None) for x in ['url', 'documentId', 'filename', 'hash', 'software', 'success']] [data.pop(x, None) for x in ['url', 'documentId', 'filename', 'hash', 'software', 'success']]
self.data = self.schema.load(data) self.data = self.schema.load(data)
for dev in self.data['devices']: for dev in self.data['devices']:
if not hasattr(dev, 'components'): if not hasattr(dev, 'components'):
continue continue

View File

@ -434,3 +434,4 @@ class DocumentDef(Resource):
auth=app.auth) auth=app.auth)
wbconf_view = app.auth.requires_auth(wbconf_view) wbconf_view = app.auth.requires_auth(wbconf_view)
self.add_url_rule('/wbconf/<string:wbtype>', view_func=wbconf_view, methods=get) self.add_url_rule('/wbconf/<string:wbtype>', view_func=wbconf_view, methods=get)

View File

@ -1,14 +1,23 @@
from citext import CIText
from flask import g from flask import g
from citext import CIText
from sortedcontainers import SortedSet
from sqlalchemy import BigInteger, Column, Sequence, Unicode, Boolean, ForeignKey from sqlalchemy import BigInteger, Column, Sequence, Unicode, Boolean, ForeignKey
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.dialects.postgresql import UUID 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.db import db
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.models import Thing, STR_SM_SIZE from ereuse_devicehub.resources.models import Thing, STR_SM_SIZE
_sorted_documents = {
'order_by': lambda: Document.created,
'collection_class': SortedSet
}
class Document(Thing): class Document(Thing):
"""This represent a generic document.""" """This represent a generic document."""

View File

@ -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 import post_load
from marshmallow.validate import Range
from teal.marshmallow import SanitizedStr, URL from teal.marshmallow import SanitizedStr, URL
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.documents import models as m from ereuse_devicehub.resources.documents import models as m

View File

@ -1,4 +1,3 @@
import copy
from citext import CIText from citext import CIText
from flask import g from flask import g
@ -71,6 +70,8 @@ class TradeDocument(Thing):
file_hash.comment = """This is the hash of the file produced from frontend.""" file_hash.comment = """This is the hash of the file produced from frontend."""
url = db.Column(URL()) url = db.Column(URL())
url.comment = """This is the url where resides the document.""" 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__ = ( __table_args__ = (
db.Index('document_id', id, postgresql_using='hash'), db.Index('document_id', id, postgresql_using='hash'),
@ -100,9 +101,8 @@ class TradeDocument(Thing):
revoke = 'Revoke' revoke = 'Revoke'
revoke_pending = 'Revoke Pending' revoke_pending = 'Revoke Pending'
confirm_revoke = 'Document Revoked' confirm_revoke = 'Document Revoked'
if not self.actions: if not self.actions:
return return
ac = self.actions[-1] ac = self.actions[-1]
@ -110,7 +110,7 @@ class TradeDocument(Thing):
# can to do revoke_confirmed # can to do revoke_confirmed
return confirm_revoke return confirm_revoke
if ac.type == 'RevokeDocument': if ac.type == 'RevokeDocument':
if ac.user == g.user: if ac.user == g.user:
# can todo revoke_pending # can todo revoke_pending
return revoke_pending return revoke_pending
@ -130,9 +130,22 @@ class TradeDocument(Thing):
# can to do revoke # can to do revoke
return double_confirm return double_confirm
def _warning_actions(self, actions): @property
return sorted(ev for ev in actions if ev.severity >= Severity.Warning) 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): def __lt__(self, other):
return self.id < other.id return self.id < other.id

View File

@ -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 teal.marshmallow import SanitizedStr, URL
# from marshmallow import ValidationError, validates_schema # from marshmallow import ValidationError, validates_schema
@ -29,3 +29,5 @@ class TradeDocument(Thing):
url = URL(description=m.TradeDocument.url.comment) url = URL(description=m.TradeDocument.url.comment)
lot = NestedOn('Lot', only_query='id', description=m.TradeDocument.lot.__doc__) lot = NestedOn('Lot', only_query='id', description=m.TradeDocument.lot.__doc__)
trading = SanitizedStr(dump_only=True, description='') 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)

View File

@ -28,6 +28,7 @@ from ereuse_devicehub.resources.action import models
from ereuse_devicehub.resources.device import states from ereuse_devicehub.resources.device import states
from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \ from ereuse_devicehub.resources.device.models import Desktop, Device, GraphicCard, HardDrive, \
RamModule, SolidStateDrive RamModule, SolidStateDrive
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.enums import ComputerChassis, Severity, TestDataStorageLength from ereuse_devicehub.resources.enums import ComputerChassis, Severity, TestDataStorageLength
from tests import conftest from tests import conftest
from tests.conftest import create_user, file, yaml2json, json_encode 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 "alert alert-info" in response
assert "100% coincidence." in response assert "100% coincidence." in response
assert not "alert alert-danger" 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)

View File

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

View File

@ -16,6 +16,7 @@ from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.user.models import Session from ereuse_devicehub.resources.user.models import Session
from ereuse_devicehub.resources.action.models import Snapshot, Allocate, Live from ereuse_devicehub.resources.action.models import Snapshot, Allocate, Live
from ereuse_devicehub.resources.documents import documents 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.device import models as d
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
@ -677,3 +678,41 @@ def test_get_wbconf(user: UserClient):
assert token in env assert token in env
user.user['token'] = token user.user['token'] = token
snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot) 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']