Merge pull request #145 from eReuse/feature/trade-documents

Feature/trade documents
This commit is contained in:
cayop 2021-07-01 12:39:13 +02:00 committed by GitHub
commit bc0e94d973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 779 additions and 52 deletions

View File

@ -12,6 +12,7 @@ from ereuse_devicehub.resources import action, agent, deliverynote, inventory, \
lot, tag, user
from ereuse_devicehub.resources.device import definitions
from ereuse_devicehub.resources.documents import documents
from ereuse_devicehub.resources.tradedocument import definitions as tradedocument
from ereuse_devicehub.resources.enums import PriceSoftware
from ereuse_devicehub.resources.versions import versions
from ereuse_devicehub.resources.licences import licences
@ -27,6 +28,7 @@ class DevicehubConfig(Config):
import_resource(lot),
import_resource(deliverynote),
import_resource(documents),
import_resource(tradedocument),
import_resource(inventory),
import_resource(versions),
import_resource(licences),
@ -71,3 +73,6 @@ class DevicehubConfig(Config):
"""Admin email"""
EMAIL_ADMIN = config('EMAIL_ADMIN', '')
"""Definition of path where save the documents of customers"""
PATH_DOCUMENTS_STORAGE = config('PATH_DOCUMENTS_STORAGE', '/tmp/')

View File

@ -0,0 +1,134 @@
"""tradeDocuments
Revision ID: 3a3601ac8224
Revises: 51439cf24be8
Create Date: 2021-06-15 14:38:59.931818
"""
import teal
import citext
import sqlalchemy as sa
from alembic import op
from alembic import context
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '3a3601ac8224'
down_revision = '51439cf24be8'
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.create_table('trade_document',
sa.Column(
'updated',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='The last time Devicehub recorded a change for \n this thing.\n '
),
sa.Column(
'created',
sa.TIMESTAMP(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'),
nullable=False,
comment='When Devicehub created this.'
),
sa.Column(
'id',
sa.BigInteger(),
nullable=False,
comment='The identifier of the device for this database. Used only\n internally for software; users should not use this.\n '
),
sa.Column(
'date',
sa.DateTime(),
nullable=True,
comment='The date of document, some documents need to have one date\n '
),
sa.Column(
'id_document',
citext.CIText(),
nullable=True,
comment='The id of one document like invoice so they can be linked.'
),
sa.Column(
'description',
citext.CIText(),
nullable=True,
comment='A description of document.'
),
sa.Column(
'owner_id',
postgresql.UUID(as_uuid=True),
nullable=False
),
sa.Column(
'lot_id',
postgresql.UUID(as_uuid=True),
nullable=False
),
sa.Column(
'file_name',
citext.CIText(),
nullable=True,
comment='This is the name of the file when user up the document.'
),
sa.Column(
'file_hash',
citext.CIText(),
nullable=True,
comment='This is the hash of the file produced from frontend.'
),
sa.Column(
'url',
citext.CIText(),
teal.db.URL(),
nullable=True,
comment='This is the url where resides the document.'
),
sa.ForeignKeyConstraint(['lot_id'], [f'{get_inv()}.lot.id'],),
sa.ForeignKeyConstraint(['owner_id'], ['common.user.id'],),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}'
)
# Action document table
op.create_table('action_trade_document',
sa.Column('document_id', sa.BigInteger(), nullable=False),
sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['action_id'], [f'{get_inv()}.action.id'], ),
sa.ForeignKeyConstraint(['document_id'], [f'{get_inv()}.trade_document.id'], ),
sa.PrimaryKeyConstraint('document_id', 'action_id'),
schema=f'{get_inv()}'
)
op.create_index('document_id', 'trade_document', ['id'], unique=False, postgresql_using='hash', schema=f'{get_inv()}')
op.create_index(op.f('ix_trade_document_created'), 'trade_document', ['created'], unique=False, schema=f'{get_inv()}')
op.create_index(op.f('ix_trade_document_updated'), 'trade_document', ['updated'], unique=False, schema=f'{get_inv()}')
op.create_table('confirm_document',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('action_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ),
sa.ForeignKeyConstraint(['action_id'], [f'{get_inv()}.action.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['common.user.id'], ),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}'
)
def downgrade():
op.drop_table('action_trade_document', schema=f'{get_inv()}')
op.drop_table('confirm_document', schema=f'{get_inv()}')
op.drop_table('trade_document', schema=f'{get_inv()}')

View File

@ -5,11 +5,12 @@ Revises: eca457d8b2a4
Create Date: 2021-03-15 17:40:34.410408
"""
import sqlalchemy as sa
import citext
import teal
from alembic import op
from alembic import context
from sqlalchemy.dialects import postgresql
import sqlalchemy as sa
import citext
# revision identifiers, used by Alembic.
@ -83,7 +84,7 @@ def upgrade():
schema=f'{get_inv()}'
)
# ## User
## User
op.add_column('user', sa.Column('active', sa.Boolean(), default=True, nullable=True),
schema='common')
op.add_column('user', sa.Column('phantom', sa.Boolean(), default=False, nullable=True),

View File

@ -270,6 +270,21 @@ class TradeDef(ActionDef):
SCHEMA = schemas.Trade
class ConfirmDocumentDef(ActionDef):
VIEW = None
SCHEMA = schemas.ConfirmDocument
class RevokeDocumentDef(ActionDef):
VIEW = None
SCHEMA = schemas.RevokeDocument
class ConfirmRevokeDocumentDef(ActionDef):
VIEW = None
SCHEMA = schemas.ConfirmRevokeDocument
class CancelTradeDef(ActionDef):
VIEW = None
SCHEMA = schemas.CancelTrade

View File

@ -48,6 +48,7 @@ from ereuse_devicehub.resources.enums import AppearanceRange, BatteryHealth, Bio
TestDataStorageLength
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
class JoinedTableMixin:
@ -295,6 +296,20 @@ class ActionDevice(db.Model):
primary_key=True)
class ActionWithMultipleTradeDocuments(ActionWithMultipleDevices):
documents = relationship(TradeDocument,
backref=backref('actions_docs', lazy=True, **_sorted_actions),
secondary=lambda: ActionTradeDocument.__table__,
order_by=lambda: TradeDocument.id,
collection_class=OrderedSet)
class ActionTradeDocument(db.Model):
document_id = Column(BigInteger, ForeignKey(TradeDocument.id), primary_key=True)
action_id = Column(UUID(as_uuid=True), ForeignKey(ActionWithMultipleTradeDocuments.id),
primary_key=True)
class Add(ActionWithOneDevice):
"""The act of adding components to a device.
@ -1433,6 +1448,43 @@ class CancelReservation(Organize):
"""The act of cancelling a reservation."""
class ConfirmDocument(JoinedTableMixin, ActionWithMultipleTradeDocuments):
"""Users confirm the one action trade this confirmation it's link to trade
and the document that confirm
"""
user_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
user = db.relationship(User, primaryjoin=user_id == User.id)
user_comment = """The user that accept the offer."""
action_id = db.Column(UUID(as_uuid=True),
db.ForeignKey('action.id'),
nullable=False)
action = db.relationship('Action',
backref=backref('acceptances_document',
uselist=True,
lazy=True,
order_by=lambda: Action.end_time,
collection_class=list),
primaryjoin='ConfirmDocument.action_id == Action.id')
def __repr__(self) -> str:
if self.action.t in ['Trade']:
origin = 'To'
if self.user == self.action.user_from:
origin = 'From'
return '<{0.t}app/views/inventory/ {0.id} accepted by {1}>'.format(self, origin)
class RevokeDocument(ConfirmDocument):
pass
class ConfirmRevokeDocument(ConfirmDocument):
pass
class Confirm(JoinedTableMixin, ActionWithMultipleDevices):
"""Users confirm the one action trade this confirmation it's link to trade
and the devices that confirm
@ -1473,7 +1525,7 @@ class ConfirmRevoke(Confirm):
return '<{0.t} {0.id} accepted by {0.user}>'.format(self)
class Trade(JoinedTableMixin, ActionWithMultipleDevices):
class Trade(JoinedTableMixin, ActionWithMultipleTradeDocuments):
"""Trade actions log the political exchange of devices between users.
Every time a trade action is performed, the old user looses its
political possession, for example ownership, in favor of another
@ -1500,8 +1552,6 @@ class Trade(JoinedTableMixin, ActionWithMultipleDevices):
currency = Column(DBEnum(Currency), nullable=False, default=Currency.EUR.name)
currency.comment = """The currency of this price as for ISO 4217."""
date = Column(db.TIMESTAMP(timezone=True))
document_id = Column(CIText())
document_id.comment = """The id of one document like invoice so they can be linked."""
confirm = Column(Boolean, default=False, nullable=False)
confirm.comment = """If you need confirmation of the user, you need actevate this field"""
code = Column(CIText(), nullable=True)

View File

@ -16,6 +16,7 @@ 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.tradedocument import schemas as s_document
from ereuse_devicehub.resources.enums import AppearanceRange, BiosAccessRange, FunctionalityRange, \
PhysicalErasureMethod, R_POSITIVE, RatingRange, \
Severity, SnapshotSoftware, TestDataStorageLength
@ -58,6 +59,15 @@ class ActionWithOneDevice(Action):
device = NestedOn(s_device.Device, only_query='id')
class ActionWithMultipleDocuments(Action):
__doc__ = m.ActionWithMultipleTradeDocuments.__doc__
documents = NestedOn(s_document.TradeDocument,
many=True,
required=True, # todo test ensuring len(devices) >= 1
only_query='id',
collection_class=OrderedSet)
class ActionWithMultipleDevices(Action):
__doc__ = m.ActionWithMultipleDevices.__doc__
devices = NestedOn(s_device.Device,
@ -482,6 +492,126 @@ class Revoke(ActionWithMultipleDevices):
txt = "Device {} not exist in the trade".format(dev.devicehub_id)
raise ValidationError(txt)
for doc in data.get('documents', []):
# if document not exist in the Trade, then this query is wrong
if not doc in data['action'].documents:
txt = "Document {} not exist in the trade".format(doc.file_name)
raise ValidationError(txt)
@validates_schema
def validate_documents(self, data):
"""Check if there are or no one before confirmation,
This is not checked in the view becouse the list of documents is inmutable
"""
if not data['devices'] == OrderedSet():
return
documents = []
for doc in data['documents']:
actions = copy.copy(doc.actions)
actions.reverse()
for ac in actions:
if ac == data['action']:
# data['action'] is a Trade action, if this is the first action
# to find mean that this document don't have a confirmation
break
if ac.t == 'Revoke' and ac.user == g.user:
# this doc is confirmation jet
break
if ac.t == Confirm.t and ac.user == g.user:
documents.append(doc)
break
if not documents:
txt = 'No there are documents to revoke'
raise ValidationError(txt)
class ConfirmDocument(ActionWithMultipleDocuments):
__doc__ = m.Confirm.__doc__
action = NestedOn('Action', only_query='id')
@validates_schema
def validate_documents(self, data):
"""If there are one device than have one confirmation,
then remove the list this device of the list of devices of this action
"""
# import pdb; pdb.set_trace()
if data['documents'] == OrderedSet():
return
for doc in data['documents']:
if not doc.lot.trade:
return
data['action'] = doc.lot.trade
if not doc.actions:
continue
if not doc.trading == 'Need Confirmation':
txt = 'No there are documents to confirm'
raise ValidationError(txt)
class RevokeDocument(ActionWithMultipleDocuments):
__doc__ = m.RevokeDocument.__doc__
action = NestedOn('Action', only_query='id')
@validates_schema
def validate_documents(self, data):
"""Check if there are or no one before confirmation,
This is not checked in the view becouse the list of documents is inmutable
"""
# import pdb; pdb.set_trace()
if data['documents'] == OrderedSet():
return
for doc in data['documents']:
if not doc.lot.trade:
return
data['action'] = doc.lot.trade
if not doc.actions:
continue
if not doc.trading in ['Document Confirmed', 'Confirm']:
txt = 'No there are documents to revoke'
raise ValidationError(txt)
class ConfirmRevokeDocument(ActionWithMultipleDocuments):
__doc__ = m.ConfirmRevoke.__doc__
action = NestedOn('Action', only_query='id')
@validates_schema
def validate_documents(self, data):
"""Check if there are or no one before confirmation,
This is not checked in the view becouse the list of documents is inmutable
"""
if data['documents'] == OrderedSet():
return
for doc in data['documents']:
if not doc.lot.trade:
return
if not doc.actions:
continue
if not doc.trading == 'Revoke':
txt = 'No there are documents with revoke for confirm'
raise ValidationError(txt)
data['action'] = doc.actions[-1]
class ConfirmRevoke(ActionWithMultipleDevices):
__doc__ = m.ConfirmRevoke.__doc__
@ -489,17 +619,62 @@ class ConfirmRevoke(ActionWithMultipleDevices):
@validates_schema
def validate_revoke(self, data: dict):
# import pdb; pdb.set_trace()
for dev in data['devices']:
# if device not exist in the Trade, then this query is wrong
if not dev in data['action'].devices:
txt = "Device {} not exist in the revoke action".format(dev.devicehub_id)
txt = "Device {} not exist in the trade".format(dev.devicehub_id)
raise ValidationError(txt)
for doc in data.get('documents', []):
# if document not exist in the Trade, then this query is wrong
if not doc in data['action'].documents:
txt = "Document {} not exist in the trade".format(doc.file_name)
raise ValidationError(txt)
@validates_schema
def validate_docs(self, data):
"""Check if there are or no one before confirmation,
This is not checked in the view becouse the list of documents is inmutable
"""
if not data['devices'] == OrderedSet():
return
documents = []
for doc in data['documents']:
actions = copy.copy(doc.actions)
actions.reverse()
for ac in actions:
if ac == data['action']:
# If document have the last action the action for confirm
documents.append(doc)
break
if ac.t == 'Revoke' and not ac.user == g.user:
# If document is revoke before you can Confirm now
# and revoke is an action of one other user
documents.append(doc)
break
if ac.t == ConfirmRevoke.t and ac.user == g.user:
# If document is confirmed we don't need confirmed again
break
if ac.t == Confirm.t:
# if onwer of trade confirm again before than this user Confirm the
# revoke, then is not possible confirm the revoke
#
# If g.user confirm the trade before do a ConfirmRevoke
# then g.user can not to do the ConfirmRevoke more
break
if not documents:
txt = 'No there are documents with revoke for confirm'
raise ValidationError(txt)
class Trade(ActionWithMultipleDevices):
__doc__ = m.Trade.__doc__
document_id = SanitizedStr(validate=Length(max=STR_SIZE), data_key='documentID', required=False)
date = DateTime(data_key='date', required=False)
price = Float(required=False, data_key='price')
user_to_email = SanitizedStr(
@ -542,7 +717,13 @@ class Trade(ActionWithMultipleDevices):
txt = "you need to be the owner of the lot for to do a trade"
raise ValidationError(txt)
for doc in data['lot'].documents:
if not doc.owner == g.user:
txt = "you need to be the owner of the documents for to do a trade"
raise ValidationError(txt)
data['devices'] = data['lot'].devices
data['documents'] = data['lot'].documents
@validates_schema
def validate_user_to_email(self, data: dict):

View File

@ -1,11 +1,11 @@
import copy
from flask import g
from sqlalchemy.util import OrderedSet
from teal.marshmallow import ValidationError
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.action.models import Trade, Confirm, ConfirmRevoke, Revoke
from ereuse_devicehub.resources.action.models import (Trade, Confirm, ConfirmRevoke,
Revoke, RevokeDocument, ConfirmDocument,
ConfirmRevokeDocument)
from ereuse_devicehub.resources.user.models import User
from ereuse_devicehub.resources.lot.views import delete_from_trade
@ -16,11 +16,11 @@ class TradeView():
request_post = {
'type': 'Trade',
'devices': [device_id],
'documents': [document_id],
'userFrom': user2.email,
'userTo': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirm': True,
}
@ -51,10 +51,17 @@ class TradeView():
# if the confirmation is mandatory, do automatic confirmation only for
# owner of the lot
if self.trade.confirm:
confirm = Confirm(user=g.user,
action=self.trade,
devices=self.trade.devices)
db.session.add(confirm)
if self.trade.devices:
confirm_devs = Confirm(user=g.user,
action=self.trade,
devices=self.trade.devices)
db.session.add(confirm_devs)
if self.trade.documents:
confirm_docs = ConfirmDocument(user=g.user,
action=self.trade,
documents=self.trade.documents)
db.session.add(confirm_docs)
return
# check than the user than want to do the action is one of the users
@ -173,7 +180,6 @@ class ConfirmView(ConfirmMixin):
"""If there are one device than have one confirmation,
then remove the list this device of the list of devices of this action
"""
# import pdb; pdb.set_trace()
real_devices = []
for dev in data['devices']:
ac = dev.last_action_trading
@ -261,3 +267,111 @@ class ConfirmRevokeView(ConfirmMixin):
dev.reset_owner()
trade.lot.devices.difference_update(devices)
class ConfirmDocumentMixin():
"""
Very Important:
==============
All of this Views than inherit of this class is executed for users
than is not owner of the Trade action.
The owner of Trade action executed this actions of confirm and revoke from the
lot
"""
Model = None
def __init__(self, data, resource_def, schema):
# import pdb; pdb.set_trace()
self.schema = schema
a = resource_def.schema.load(data)
self.validate(a)
if not a['documents']:
raise ValidationError('Documents not exist.')
self.model = self.Model(**a)
def post(self):
db.session().final_flush()
ret = self.schema.jsonify(self.model)
ret.status_code = 201
db.session.commit()
return ret
class ConfirmDocumentView(ConfirmDocumentMixin):
"""Handler for manager the Confirmation register from post
request_confirm = {
'type': 'Confirm',
'action': trade.id,
'documents': [document_id],
}
"""
Model = ConfirmDocument
def validate(self, data):
"""If there are one device than have one confirmation,
then remove the list this device of the list of devices of this action
"""
for doc in data['documents']:
ac = doc.trading
if not doc.trading in ['Confirm', 'Need Confirmation']:
txt = 'Some of documents do not have enough to confirm for to do a Doble Confirmation'
ValidationError(txt)
### End check ###
class RevokeDocumentView(ConfirmDocumentMixin):
"""Handler for manager the Revoke register from post
request_revoke = {
'type': 'Revoke',
'action': trade.id,
'documents': [document_id],
}
"""
Model = RevokeDocument
def validate(self, data):
"""All devices need to have the status of DoubleConfirmation."""
### check ###
if not data['documents']:
raise ValidationError('Documents not exist.')
for doc in data['documents']:
if not doc.trading in ['Document Confirmed', 'Confirm']:
txt = 'Some of documents do not have enough to confirm for to do a revoke'
ValidationError(txt)
### End check ###
class ConfirmRevokeDocumentView(ConfirmDocumentMixin):
"""Handler for manager the Confirmation register from post
request_confirm_revoke = {
'type': 'ConfirmRevoke',
'action': action_revoke.id,
'documents': [document_id],
}
"""
Model = ConfirmRevokeDocument
def validate(self, data):
"""All devices need to have the status of revoke."""
if not data['action'].type == 'RevokeDocument':
txt = 'Error: this action is not a revoke action'
ValidationError(txt)
for doc in data['documents']:
if not doc.trading == 'Revoke':
txt = 'Some of documents do not have revoke to confirm'
ValidationError(txt)

View File

@ -202,6 +202,19 @@ class ActionView(View):
confirm_revoke = trade_view.ConfirmRevokeView(json, resource_def, self.schema)
return confirm_revoke.post()
if json['type'] == 'RevokeDocument':
revoke = trade_view.RevokeDocumentView(json, resource_def, self.schema)
return revoke.post()
if json['type'] == 'ConfirmDocument':
confirm = trade_view.ConfirmDocumentView(json, resource_def, self.schema)
return confirm.post()
if json['type'] == 'ConfirmRevokeDocument':
confirm_revoke = trade_view.ConfirmRevokeDocumentView(json, resource_def, self.schema)
return confirm_revoke.post()
# import pdb; pdb.set_trace()
a = resource_def.schema.load(json)
Model = db.Model._decl_class_registry.data[json['type']]()
action = Model(**a)

View File

@ -625,11 +625,11 @@ class Computer(Device):
It is a subset of the Linux definition of DMI / DMI decode.
"""
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
owner_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
author = db.relationship(User, primaryjoin=owner_id == User.id)
# owner_id = db.Column(UUID(as_uuid=True),
# db.ForeignKey(User.id),
# nullable=False,
# default=lambda: g.user.id)
# author = db.relationship(User, primaryjoin=owner_id == User.id)
transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False)
transfer_state.comment = TransferState.__doc__
receiver_id = db.Column(UUID(as_uuid=True),

View File

@ -50,7 +50,7 @@ class Device(Thing):
description='The lots where this device is directly under.')
rate = NestedOn('Rate', dump_only=True, description=m.Device.rate.__doc__)
price = NestedOn('Price', dump_only=True, description=m.Device.price.__doc__)
trading = EnumField(states.Trading, dump_only=True, description=m.Device.trading.__doc__)
# trading = EnumField(states.Trading, dump_only=True, description=m.Device.trading.__doc__)
trading = SanitizedStr(dump_only=True, description='')
physical = EnumField(states.Physical, dump_only=True, description=m.Device.physical.__doc__)
traking= EnumField(states.Traking, dump_only=True, description=m.Device.physical.__doc__)

View File

@ -5,6 +5,7 @@ from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.deliverynote import schemas as s_deliverynote
from ereuse_devicehub.resources.device import schemas as s_device
from ereuse_devicehub.resources.action import schemas as s_action
from ereuse_devicehub.resources.tradedocument import schemas as s_document
from ereuse_devicehub.resources.enums import TransferState
from ereuse_devicehub.resources.lot import models as m
from ereuse_devicehub.resources.models import STR_SIZE
@ -27,4 +28,5 @@ class Lot(Thing):
transfer_state = EnumField(TransferState, description=m.Lot.transfer_state.comment)
receiver_address = SanitizedStr(validate=f.validate.Length(max=42))
deliverynote = NestedOn(s_deliverynote.Deliverynote, dump_only=True)
documents = NestedOn('TradeDocument', many=True, dump_only=True)
trade = NestedOn(s_action.Trade, dump_only=True)

View File

@ -0,0 +1,10 @@
from teal.resource import Converters, Resource
from ereuse_devicehub.resources.tradedocument import schemas
from ereuse_devicehub.resources.tradedocument.views import TradeDocumentView
class TradeDocumentDef(Resource):
SCHEMA = schemas.TradeDocument
VIEW = TradeDocumentView
AUTH = True
ID_CONVERTER = Converters.string

View File

@ -0,0 +1,141 @@
import copy
from citext import CIText
from flask import g
from sqlalchemy.dialects.postgresql import UUID
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.user.models import User
from sortedcontainers import SortedSet
from ereuse_devicehub.resources.models import Thing
from sqlalchemy import BigInteger, Column, Sequence
from sqlalchemy.orm import backref
from teal.db import CASCADE_OWN, URL
from ereuse_devicehub.resources.enums import Severity
_sorted_documents = {
'order_by': lambda: TradeDocument.created,
'collection_class': SortedSet
}
class TradeDocument(Thing):
"""This represent a document involved in a trade action.
Every document is added to a lot.
When this lot is converted in one trade, the action trade is added to the document
and the action trade need to be confirmed for the both users of the trade.
This confirmation can be revoked and this revoked need to be ConfirmRevoke for have
some efect.
This documents can be invoices or list of devices or certificates of erasure of
one disk.
Like a Devices one document have actions and is possible add or delete of one lot
if this lot don't have a trade
The document is saved in the database
"""
id = Column(BigInteger, Sequence('device_seq'), primary_key=True)
id.comment = """The identifier of the device for this database. Used only
internally for software; users should not use this.
"""
# type = Column(Unicode(STR_SM_SIZE), nullable=False)
date = Column(db.DateTime)
date.comment = """The date of document, some documents need to have one date
"""
id_document = Column(CIText())
id_document.comment = """The id of one document like invoice so they can be linked."""
description = Column(db.CIText())
description.comment = """A description of document."""
owner_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
owner = db.relationship(User, primaryjoin=owner_id == User.id)
lot_id = db.Column(UUID(as_uuid=True),
db.ForeignKey('lot.id'),
nullable=False)
lot = db.relationship('Lot',
backref=backref('documents',
lazy=True,
cascade=CASCADE_OWN,
**_sorted_documents),
primaryjoin='TradeDocument.lot_id == Lot.id')
lot.comment = """Lot to which the document is associated"""
file_name = Column(db.CIText())
file_name.comment = """This is the name of the file when user up the document."""
file_hash = Column(db.CIText())
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."""
__table_args__ = (
db.Index('document_id', id, postgresql_using='hash'),
# db.Index('type_doc', type, postgresql_using='hash')
)
@property
def actions(self) -> list:
"""All the actions where the device participated, including:
1. Actions performed directly to the device.
2. Actions performed to a component.
3. Actions performed to a parent device.
Actions are returned by descending ``created`` time.
"""
return sorted(self.actions_docs, key=lambda x: x.created)
@property
def trading(self):
"""The trading state, or None if no Trade action has
ever been performed to this device. This extract the posibilities for to do"""
confirm = 'Confirm'
need_confirm = 'Need Confirmation'
double_confirm = 'Document Confirmed'
revoke = 'Revoke'
revoke_pending = 'Revoke Pending'
confirm_revoke = 'Document Revoked'
if not self.actions:
return
ac = self.actions[-1]
if ac.type == 'ConfirmRevokeDocument':
# can to do revoke_confirmed
return confirm_revoke
if ac.type == 'RevokeDocument':
if ac.user == g.user:
# can todo revoke_pending
return revoke_pending
else:
# can to do confirm_revoke
return revoke
if ac.type == 'ConfirmDocument':
if ac.user == self.owner:
if self.owner == g.user:
# can to do revoke
return confirm
else:
# can to do confirm
return need_confirm
else:
# can to do revoke
return double_confirm
def _warning_actions(self, actions):
return sorted(ev for ev in actions if ev.severity >= Severity.Warning)
def __lt__(self, other):
return self.id < other.id
def __str__(self) -> str:
return '{0.file_name}'.format(self)

View File

@ -0,0 +1,31 @@
from marshmallow.fields import DateTime, Integer, validate
from teal.marshmallow import SanitizedStr, URL
# from marshmallow import ValidationError, validates_schema
from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.tradedocument import models as m
# from ereuse_devicehub.resources.lot import schemas as s_lot
class TradeDocument(Thing):
__doc__ = m.TradeDocument.__doc__
id = Integer(description=m.TradeDocument.id.comment, dump_only=True)
date = DateTime(required=False, description=m.TradeDocument.date.comment)
id_document = SanitizedStr(data_key='documentId',
default='',
description=m.TradeDocument.id_document.comment)
description = SanitizedStr(default='',
description=m.TradeDocument.description.comment,
validate=validate.Length(max=500))
file_name = SanitizedStr(data_key='filename',
default='',
description=m.TradeDocument.file_name.comment,
validate=validate.Length(max=100))
file_hash = SanitizedStr(data_key='hash',
default='',
description=m.TradeDocument.file_hash.comment,
validate=validate.Length(max=64))
url = URL(description=m.TradeDocument.url.comment)
lot = NestedOn('Lot', only_query='id', description=m.TradeDocument.lot.__doc__)
trading = SanitizedStr(dump_only=True, description='')

View File

@ -0,0 +1,47 @@
import os
import time
from datetime import datetime
from flask import current_app as app, request, g, Response
from teal.resource import View
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.tradedocument.models import TradeDocument
from ereuse_devicehub.resources.action.models import ConfirmDocument
from ereuse_devicehub.resources.hash_reports import ReportHash
class TradeDocumentView(View):
def one(self, id: str):
doc = TradeDocument.query.filter_by(id=id, owner=g.user).one()
return self.schema.jsonify(doc)
def post(self):
"""Add one document."""
data = request.get_json(validate=True)
hash3 = data['file_hash']
db_hash = ReportHash(hash3=hash3)
db.session.add(db_hash)
doc = TradeDocument(**data)
trade = doc.lot.trade
if trade:
trade.documents.add(doc)
confirm = ConfirmDocument(action=trade,
user=g.user,
devices=set(),
documents={doc})
db.session.add(confirm)
db.session.add(doc)
db.session().final_flush()
ret = self.schema.jsonify(doc)
ret.status_code = 201
db.session.commit()
return ret
def delete(self, id):
doc = TradeDocument.query.filter_by(id=id, owner=g.user).one()
db.session.delete(doc)
db.session.commit()
return Response(status=204)

View File

@ -59,6 +59,13 @@ class User(Thing):
"""The individual associated for this database, or None."""
return next(iter(self.individuals), None)
@property
def code(self):
"""Code of phantoms accounts"""
if not self.phantom:
return
return self.email.split('@')[0].split('_')[1]
class UserInventory(db.Model):
"""Relationship between users and their inventories."""

View File

@ -23,6 +23,7 @@ class User(Thing):
description='Use this token in an Authorization header to access the app.'
'The token can change overtime.')
inventories = NestedOn(Inventory, many=True, dump_only=True)
code = String(dump_only=True, description='Code of inactive accounts')
def __init__(self,
only=None,

View File

@ -35,6 +35,7 @@ class TestConfig(DevicehubConfig):
TMP_SNAPSHOTS = '/tmp/snapshots'
TMP_LIVES = '/tmp/lives'
EMAIL_ADMIN = 'foo@foo.com'
PATH_DOCUMENTS_STORAGE = '/tmp/trade_documents'
@pytest.fixture(scope='session')

View File

@ -765,7 +765,6 @@ def test_trade_endpoint(user: UserClient, user2: UserClient):
device2, _ = user2.get(res=Device, item=device['id'])
assert device2['id'] == device['id']
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_offer_without_to(user: UserClient):
@ -789,7 +788,6 @@ def test_offer_without_to(user: UserClient):
'userFromEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': False,
'code': 'MAX'
@ -817,7 +815,6 @@ def test_offer_without_to(user: UserClient):
'userFromEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': False,
'code': 'MAX'
@ -840,7 +837,6 @@ def test_offer_without_to(user: UserClient):
'userFromEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot2.id,
'confirms': False,
'code': 'MAX'
@ -871,7 +867,6 @@ def test_offer_without_from(user: UserClient, user2: UserClient):
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot.id,
'confirms': False,
'code': 'MAX'
@ -916,7 +911,6 @@ def test_offer_without_users(user: UserClient):
'devices': [device.id],
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot.id,
'confirms': False,
'code': 'MAX'
@ -950,7 +944,6 @@ def test_offer(user: UserClient):
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot.id,
'confirms': True,
}
@ -977,7 +970,6 @@ def test_offer_without_devices(user: UserClient):
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1036,7 +1028,6 @@ def test_erase_physical():
db.session.add(erasure)
db.session.commit()
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_endpoint_confirm(user: UserClient, user2: UserClient):
@ -1056,7 +1047,6 @@ def test_endpoint_confirm(user: UserClient, user2: UserClient):
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1097,7 +1087,6 @@ def test_confirm_revoke(user: UserClient, user2: UserClient):
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1172,7 +1161,6 @@ def test_usecase_confirmation(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1362,7 +1350,6 @@ def test_confirmRevoke(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1477,7 +1464,6 @@ def test_trade_case1(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1538,7 +1524,6 @@ def test_trade_case2(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1603,7 +1588,6 @@ def test_trade_case3(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1661,7 +1645,6 @@ def test_trade_case4(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1727,7 +1710,6 @@ def test_trade_case5(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1793,7 +1775,6 @@ def test_trade_case6(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1861,7 +1842,6 @@ def test_trade_case7(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -1927,7 +1907,6 @@ def test_trade_case8(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -2000,7 +1979,6 @@ def test_trade_case9(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -2081,7 +2059,6 @@ def test_trade_case10(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -2166,7 +2143,6 @@ def test_trade_case11(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -2235,7 +2211,6 @@ def test_trade_case12(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -2310,7 +2285,6 @@ def test_trade_case13(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}
@ -2385,7 +2359,6 @@ def test_trade_case14(user: UserClient, user2: UserClient):
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'documentID': '1',
'lot': lot['id'],
'confirms': True,
}

View File

@ -55,6 +55,7 @@ def test_api_docs(client: Client):
'/metrics/',
'/tags/',
'/tags/{tag_id}/device/{device_id}',
'/trade-documents/',
'/users/',
'/users/login/',
'/users/logout/',
@ -121,4 +122,4 @@ def test_api_docs(client: Client):
'scheme': 'basic',
'name': 'Authorization'
}
assert len(docs['definitions']) == 121
assert len(docs['definitions']) == 125