Merge pull request #145 from eReuse/feature/trade-documents
Feature/trade documents
This commit is contained in:
commit
bc0e94d973
|
@ -12,6 +12,7 @@ from ereuse_devicehub.resources import action, agent, deliverynote, inventory, \
|
||||||
lot, tag, user
|
lot, tag, user
|
||||||
from ereuse_devicehub.resources.device import definitions
|
from ereuse_devicehub.resources.device import definitions
|
||||||
from ereuse_devicehub.resources.documents import documents
|
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.enums import PriceSoftware
|
||||||
from ereuse_devicehub.resources.versions import versions
|
from ereuse_devicehub.resources.versions import versions
|
||||||
from ereuse_devicehub.resources.licences import licences
|
from ereuse_devicehub.resources.licences import licences
|
||||||
|
@ -27,6 +28,7 @@ class DevicehubConfig(Config):
|
||||||
import_resource(lot),
|
import_resource(lot),
|
||||||
import_resource(deliverynote),
|
import_resource(deliverynote),
|
||||||
import_resource(documents),
|
import_resource(documents),
|
||||||
|
import_resource(tradedocument),
|
||||||
import_resource(inventory),
|
import_resource(inventory),
|
||||||
import_resource(versions),
|
import_resource(versions),
|
||||||
import_resource(licences),
|
import_resource(licences),
|
||||||
|
@ -71,3 +73,6 @@ class DevicehubConfig(Config):
|
||||||
|
|
||||||
"""Admin email"""
|
"""Admin email"""
|
||||||
EMAIL_ADMIN = config('EMAIL_ADMIN', '')
|
EMAIL_ADMIN = config('EMAIL_ADMIN', '')
|
||||||
|
|
||||||
|
"""Definition of path where save the documents of customers"""
|
||||||
|
PATH_DOCUMENTS_STORAGE = config('PATH_DOCUMENTS_STORAGE', '/tmp/')
|
||||||
|
|
|
@ -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()}')
|
||||||
|
|
|
@ -5,11 +5,12 @@ Revises: eca457d8b2a4
|
||||||
Create Date: 2021-03-15 17:40:34.410408
|
Create Date: 2021-03-15 17:40:34.410408
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import citext
|
||||||
|
import teal
|
||||||
from alembic import op
|
from alembic import op
|
||||||
from alembic import context
|
from alembic import context
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
import sqlalchemy as sa
|
|
||||||
import citext
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
|
@ -83,7 +84,7 @@ def upgrade():
|
||||||
schema=f'{get_inv()}'
|
schema=f'{get_inv()}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# ## User
|
## User
|
||||||
op.add_column('user', sa.Column('active', sa.Boolean(), default=True, nullable=True),
|
op.add_column('user', sa.Column('active', sa.Boolean(), default=True, nullable=True),
|
||||||
schema='common')
|
schema='common')
|
||||||
op.add_column('user', sa.Column('phantom', sa.Boolean(), default=False, nullable=True),
|
op.add_column('user', sa.Column('phantom', sa.Boolean(), default=False, nullable=True),
|
||||||
|
|
|
@ -270,6 +270,21 @@ class TradeDef(ActionDef):
|
||||||
SCHEMA = schemas.Trade
|
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):
|
class CancelTradeDef(ActionDef):
|
||||||
VIEW = None
|
VIEW = None
|
||||||
SCHEMA = schemas.CancelTrade
|
SCHEMA = schemas.CancelTrade
|
||||||
|
|
|
@ -48,6 +48,7 @@ from ereuse_devicehub.resources.enums import AppearanceRange, BatteryHealth, Bio
|
||||||
TestDataStorageLength
|
TestDataStorageLength
|
||||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||||
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 JoinedTableMixin:
|
class JoinedTableMixin:
|
||||||
|
@ -295,6 +296,20 @@ class ActionDevice(db.Model):
|
||||||
primary_key=True)
|
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):
|
class Add(ActionWithOneDevice):
|
||||||
"""The act of adding components to a device.
|
"""The act of adding components to a device.
|
||||||
|
|
||||||
|
@ -1433,6 +1448,43 @@ class CancelReservation(Organize):
|
||||||
"""The act of cancelling a reservation."""
|
"""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):
|
class Confirm(JoinedTableMixin, ActionWithMultipleDevices):
|
||||||
"""Users confirm the one action trade this confirmation it's link to trade
|
"""Users confirm the one action trade this confirmation it's link to trade
|
||||||
and the devices that confirm
|
and the devices that confirm
|
||||||
|
@ -1473,7 +1525,7 @@ class ConfirmRevoke(Confirm):
|
||||||
return '<{0.t} {0.id} accepted by {0.user}>'.format(self)
|
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.
|
"""Trade actions log the political exchange of devices between users.
|
||||||
Every time a trade action is performed, the old user looses its
|
Every time a trade action is performed, the old user looses its
|
||||||
political possession, for example ownership, in favor of another
|
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 = Column(DBEnum(Currency), nullable=False, default=Currency.EUR.name)
|
||||||
currency.comment = """The currency of this price as for ISO 4217."""
|
currency.comment = """The currency of this price as for ISO 4217."""
|
||||||
date = Column(db.TIMESTAMP(timezone=True))
|
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 = Column(Boolean, default=False, nullable=False)
|
||||||
confirm.comment = """If you need confirmation of the user, you need actevate this field"""
|
confirm.comment = """If you need confirmation of the user, you need actevate this field"""
|
||||||
code = Column(CIText(), nullable=True)
|
code = Column(CIText(), nullable=True)
|
||||||
|
|
|
@ -16,6 +16,7 @@ from ereuse_devicehub.resources import enums
|
||||||
from ereuse_devicehub.resources.action import models as m
|
from ereuse_devicehub.resources.action import models as m
|
||||||
from ereuse_devicehub.resources.agent import schemas as s_agent
|
from ereuse_devicehub.resources.agent import schemas as s_agent
|
||||||
from ereuse_devicehub.resources.device import schemas as s_device
|
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, \
|
from ereuse_devicehub.resources.enums import AppearanceRange, BiosAccessRange, FunctionalityRange, \
|
||||||
PhysicalErasureMethod, R_POSITIVE, RatingRange, \
|
PhysicalErasureMethod, R_POSITIVE, RatingRange, \
|
||||||
Severity, SnapshotSoftware, TestDataStorageLength
|
Severity, SnapshotSoftware, TestDataStorageLength
|
||||||
|
@ -58,6 +59,15 @@ class ActionWithOneDevice(Action):
|
||||||
device = NestedOn(s_device.Device, only_query='id')
|
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):
|
class ActionWithMultipleDevices(Action):
|
||||||
__doc__ = m.ActionWithMultipleDevices.__doc__
|
__doc__ = m.ActionWithMultipleDevices.__doc__
|
||||||
devices = NestedOn(s_device.Device,
|
devices = NestedOn(s_device.Device,
|
||||||
|
@ -482,6 +492,126 @@ class Revoke(ActionWithMultipleDevices):
|
||||||
txt = "Device {} not exist in the trade".format(dev.devicehub_id)
|
txt = "Device {} not exist in the trade".format(dev.devicehub_id)
|
||||||
raise ValidationError(txt)
|
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):
|
class ConfirmRevoke(ActionWithMultipleDevices):
|
||||||
__doc__ = m.ConfirmRevoke.__doc__
|
__doc__ = m.ConfirmRevoke.__doc__
|
||||||
|
@ -489,17 +619,62 @@ class ConfirmRevoke(ActionWithMultipleDevices):
|
||||||
|
|
||||||
@validates_schema
|
@validates_schema
|
||||||
def validate_revoke(self, data: dict):
|
def validate_revoke(self, data: dict):
|
||||||
# import pdb; pdb.set_trace()
|
|
||||||
for dev in data['devices']:
|
for dev in data['devices']:
|
||||||
# if device not exist in the Trade, then this query is wrong
|
# if device not exist in the Trade, then this query is wrong
|
||||||
if not dev in data['action'].devices:
|
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)
|
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):
|
class Trade(ActionWithMultipleDevices):
|
||||||
__doc__ = m.Trade.__doc__
|
__doc__ = m.Trade.__doc__
|
||||||
document_id = SanitizedStr(validate=Length(max=STR_SIZE), data_key='documentID', required=False)
|
|
||||||
date = DateTime(data_key='date', required=False)
|
date = DateTime(data_key='date', required=False)
|
||||||
price = Float(required=False, data_key='price')
|
price = Float(required=False, data_key='price')
|
||||||
user_to_email = SanitizedStr(
|
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"
|
txt = "you need to be the owner of the lot for to do a trade"
|
||||||
raise ValidationError(txt)
|
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['devices'] = data['lot'].devices
|
||||||
|
data['documents'] = data['lot'].documents
|
||||||
|
|
||||||
@validates_schema
|
@validates_schema
|
||||||
def validate_user_to_email(self, data: dict):
|
def validate_user_to_email(self, data: dict):
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import copy
|
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from sqlalchemy.util import OrderedSet
|
from sqlalchemy.util import OrderedSet
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
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.user.models import User
|
||||||
from ereuse_devicehub.resources.lot.views import delete_from_trade
|
from ereuse_devicehub.resources.lot.views import delete_from_trade
|
||||||
|
|
||||||
|
@ -16,11 +16,11 @@ class TradeView():
|
||||||
request_post = {
|
request_post = {
|
||||||
'type': 'Trade',
|
'type': 'Trade',
|
||||||
'devices': [device_id],
|
'devices': [device_id],
|
||||||
|
'documents': [document_id],
|
||||||
'userFrom': user2.email,
|
'userFrom': user2.email,
|
||||||
'userTo': user.email,
|
'userTo': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirm': True,
|
'confirm': True,
|
||||||
}
|
}
|
||||||
|
@ -51,10 +51,17 @@ class TradeView():
|
||||||
# if the confirmation is mandatory, do automatic confirmation only for
|
# if the confirmation is mandatory, do automatic confirmation only for
|
||||||
# owner of the lot
|
# owner of the lot
|
||||||
if self.trade.confirm:
|
if self.trade.confirm:
|
||||||
confirm = Confirm(user=g.user,
|
if self.trade.devices:
|
||||||
action=self.trade,
|
confirm_devs = Confirm(user=g.user,
|
||||||
devices=self.trade.devices)
|
action=self.trade,
|
||||||
db.session.add(confirm)
|
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
|
return
|
||||||
|
|
||||||
# check than the user than want to do the action is one of the users
|
# 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,
|
"""If there are one device than have one confirmation,
|
||||||
then remove the list this device of the list of devices of this action
|
then remove the list this device of the list of devices of this action
|
||||||
"""
|
"""
|
||||||
# import pdb; pdb.set_trace()
|
|
||||||
real_devices = []
|
real_devices = []
|
||||||
for dev in data['devices']:
|
for dev in data['devices']:
|
||||||
ac = dev.last_action_trading
|
ac = dev.last_action_trading
|
||||||
|
@ -261,3 +267,111 @@ class ConfirmRevokeView(ConfirmMixin):
|
||||||
dev.reset_owner()
|
dev.reset_owner()
|
||||||
|
|
||||||
trade.lot.devices.difference_update(devices)
|
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)
|
||||||
|
|
|
@ -202,6 +202,19 @@ class ActionView(View):
|
||||||
confirm_revoke = trade_view.ConfirmRevokeView(json, resource_def, self.schema)
|
confirm_revoke = trade_view.ConfirmRevokeView(json, resource_def, self.schema)
|
||||||
return confirm_revoke.post()
|
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)
|
a = resource_def.schema.load(json)
|
||||||
Model = db.Model._decl_class_registry.data[json['type']]()
|
Model = db.Model._decl_class_registry.data[json['type']]()
|
||||||
action = Model(**a)
|
action = Model(**a)
|
||||||
|
|
|
@ -625,11 +625,11 @@ class Computer(Device):
|
||||||
It is a subset of the Linux definition of DMI / DMI decode.
|
It is a subset of the Linux definition of DMI / DMI decode.
|
||||||
"""
|
"""
|
||||||
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
|
amount = Column(Integer, check_range('amount', min=0, max=100), default=0)
|
||||||
owner_id = db.Column(UUID(as_uuid=True),
|
# owner_id = db.Column(UUID(as_uuid=True),
|
||||||
db.ForeignKey(User.id),
|
# db.ForeignKey(User.id),
|
||||||
nullable=False,
|
# nullable=False,
|
||||||
default=lambda: g.user.id)
|
# default=lambda: g.user.id)
|
||||||
author = db.relationship(User, primaryjoin=owner_id == User.id)
|
# author = db.relationship(User, primaryjoin=owner_id == User.id)
|
||||||
transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False)
|
transfer_state = db.Column(IntEnum(TransferState), default=TransferState.Initial, nullable=False)
|
||||||
transfer_state.comment = TransferState.__doc__
|
transfer_state.comment = TransferState.__doc__
|
||||||
receiver_id = db.Column(UUID(as_uuid=True),
|
receiver_id = db.Column(UUID(as_uuid=True),
|
||||||
|
|
|
@ -50,7 +50,7 @@ class Device(Thing):
|
||||||
description='The lots where this device is directly under.')
|
description='The lots where this device is directly under.')
|
||||||
rate = NestedOn('Rate', dump_only=True, description=m.Device.rate.__doc__)
|
rate = NestedOn('Rate', dump_only=True, description=m.Device.rate.__doc__)
|
||||||
price = NestedOn('Price', dump_only=True, description=m.Device.price.__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='')
|
trading = SanitizedStr(dump_only=True, description='')
|
||||||
physical = EnumField(states.Physical, dump_only=True, description=m.Device.physical.__doc__)
|
physical = EnumField(states.Physical, dump_only=True, description=m.Device.physical.__doc__)
|
||||||
traking= EnumField(states.Traking, dump_only=True, description=m.Device.physical.__doc__)
|
traking= EnumField(states.Traking, dump_only=True, description=m.Device.physical.__doc__)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from ereuse_devicehub.marshmallow import NestedOn
|
||||||
from ereuse_devicehub.resources.deliverynote import schemas as s_deliverynote
|
from ereuse_devicehub.resources.deliverynote import schemas as s_deliverynote
|
||||||
from ereuse_devicehub.resources.device import schemas as s_device
|
from ereuse_devicehub.resources.device import schemas as s_device
|
||||||
from ereuse_devicehub.resources.action import schemas as s_action
|
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.enums import TransferState
|
||||||
from ereuse_devicehub.resources.lot import models as m
|
from ereuse_devicehub.resources.lot import models as m
|
||||||
from ereuse_devicehub.resources.models import STR_SIZE
|
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)
|
transfer_state = EnumField(TransferState, description=m.Lot.transfer_state.comment)
|
||||||
receiver_address = SanitizedStr(validate=f.validate.Length(max=42))
|
receiver_address = SanitizedStr(validate=f.validate.Length(max=42))
|
||||||
deliverynote = NestedOn(s_deliverynote.Deliverynote, dump_only=True)
|
deliverynote = NestedOn(s_deliverynote.Deliverynote, dump_only=True)
|
||||||
|
documents = NestedOn('TradeDocument', many=True, dump_only=True)
|
||||||
trade = NestedOn(s_action.Trade, dump_only=True)
|
trade = NestedOn(s_action.Trade, dump_only=True)
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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='')
|
|
@ -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)
|
|
@ -59,6 +59,13 @@ class User(Thing):
|
||||||
"""The individual associated for this database, or None."""
|
"""The individual associated for this database, or None."""
|
||||||
return next(iter(self.individuals), 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):
|
class UserInventory(db.Model):
|
||||||
"""Relationship between users and their inventories."""
|
"""Relationship between users and their inventories."""
|
||||||
|
|
|
@ -23,6 +23,7 @@ class User(Thing):
|
||||||
description='Use this token in an Authorization header to access the app.'
|
description='Use this token in an Authorization header to access the app.'
|
||||||
'The token can change overtime.')
|
'The token can change overtime.')
|
||||||
inventories = NestedOn(Inventory, many=True, dump_only=True)
|
inventories = NestedOn(Inventory, many=True, dump_only=True)
|
||||||
|
code = String(dump_only=True, description='Code of inactive accounts')
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
only=None,
|
only=None,
|
||||||
|
|
|
@ -35,6 +35,7 @@ class TestConfig(DevicehubConfig):
|
||||||
TMP_SNAPSHOTS = '/tmp/snapshots'
|
TMP_SNAPSHOTS = '/tmp/snapshots'
|
||||||
TMP_LIVES = '/tmp/lives'
|
TMP_LIVES = '/tmp/lives'
|
||||||
EMAIL_ADMIN = 'foo@foo.com'
|
EMAIL_ADMIN = 'foo@foo.com'
|
||||||
|
PATH_DOCUMENTS_STORAGE = '/tmp/trade_documents'
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
|
|
|
@ -765,7 +765,6 @@ def test_trade_endpoint(user: UserClient, user2: UserClient):
|
||||||
device2, _ = user2.get(res=Device, item=device['id'])
|
device2, _ = user2.get(res=Device, item=device['id'])
|
||||||
assert device2['id'] == device['id']
|
assert device2['id'] == device['id']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.mvp
|
@pytest.mark.mvp
|
||||||
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
def test_offer_without_to(user: UserClient):
|
def test_offer_without_to(user: UserClient):
|
||||||
|
@ -789,7 +788,6 @@ def test_offer_without_to(user: UserClient):
|
||||||
'userFromEmail': user.email,
|
'userFromEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': False,
|
'confirms': False,
|
||||||
'code': 'MAX'
|
'code': 'MAX'
|
||||||
|
@ -817,7 +815,6 @@ def test_offer_without_to(user: UserClient):
|
||||||
'userFromEmail': user.email,
|
'userFromEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': False,
|
'confirms': False,
|
||||||
'code': 'MAX'
|
'code': 'MAX'
|
||||||
|
@ -840,7 +837,6 @@ def test_offer_without_to(user: UserClient):
|
||||||
'userFromEmail': user.email,
|
'userFromEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot2.id,
|
'lot': lot2.id,
|
||||||
'confirms': False,
|
'confirms': False,
|
||||||
'code': 'MAX'
|
'code': 'MAX'
|
||||||
|
@ -871,7 +867,6 @@ def test_offer_without_from(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user2.email,
|
'userToEmail': user2.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot.id,
|
'lot': lot.id,
|
||||||
'confirms': False,
|
'confirms': False,
|
||||||
'code': 'MAX'
|
'code': 'MAX'
|
||||||
|
@ -916,7 +911,6 @@ def test_offer_without_users(user: UserClient):
|
||||||
'devices': [device.id],
|
'devices': [device.id],
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot.id,
|
'lot': lot.id,
|
||||||
'confirms': False,
|
'confirms': False,
|
||||||
'code': 'MAX'
|
'code': 'MAX'
|
||||||
|
@ -950,7 +944,6 @@ def test_offer(user: UserClient):
|
||||||
'userToEmail': user2.email,
|
'userToEmail': user2.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot.id,
|
'lot': lot.id,
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -977,7 +970,6 @@ def test_offer_without_devices(user: UserClient):
|
||||||
'userToEmail': user2.email,
|
'userToEmail': user2.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1036,7 +1028,6 @@ def test_erase_physical():
|
||||||
db.session.add(erasure)
|
db.session.add(erasure)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.mvp
|
@pytest.mark.mvp
|
||||||
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
@pytest.mark.usefixtures(conftest.app_context.__name__)
|
||||||
def test_endpoint_confirm(user: UserClient, user2: UserClient):
|
def test_endpoint_confirm(user: UserClient, user2: UserClient):
|
||||||
|
@ -1056,7 +1047,6 @@ def test_endpoint_confirm(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user2.email,
|
'userToEmail': user2.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1097,7 +1087,6 @@ def test_confirm_revoke(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user2.email,
|
'userToEmail': user2.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1172,7 +1161,6 @@ def test_usecase_confirmation(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1362,7 +1350,6 @@ def test_confirmRevoke(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1477,7 +1464,6 @@ def test_trade_case1(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1538,7 +1524,6 @@ def test_trade_case2(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1603,7 +1588,6 @@ def test_trade_case3(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1661,7 +1645,6 @@ def test_trade_case4(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1727,7 +1710,6 @@ def test_trade_case5(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1793,7 +1775,6 @@ def test_trade_case6(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1861,7 +1842,6 @@ def test_trade_case7(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -1927,7 +1907,6 @@ def test_trade_case8(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -2000,7 +1979,6 @@ def test_trade_case9(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -2081,7 +2059,6 @@ def test_trade_case10(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -2166,7 +2143,6 @@ def test_trade_case11(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -2235,7 +2211,6 @@ def test_trade_case12(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -2310,7 +2285,6 @@ def test_trade_case13(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
@ -2385,7 +2359,6 @@ def test_trade_case14(user: UserClient, user2: UserClient):
|
||||||
'userToEmail': user.email,
|
'userToEmail': user.email,
|
||||||
'price': 10,
|
'price': 10,
|
||||||
'date': "2020-12-01T02:00:00+00:00",
|
'date': "2020-12-01T02:00:00+00:00",
|
||||||
'documentID': '1',
|
|
||||||
'lot': lot['id'],
|
'lot': lot['id'],
|
||||||
'confirms': True,
|
'confirms': True,
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ def test_api_docs(client: Client):
|
||||||
'/metrics/',
|
'/metrics/',
|
||||||
'/tags/',
|
'/tags/',
|
||||||
'/tags/{tag_id}/device/{device_id}',
|
'/tags/{tag_id}/device/{device_id}',
|
||||||
|
'/trade-documents/',
|
||||||
'/users/',
|
'/users/',
|
||||||
'/users/login/',
|
'/users/login/',
|
||||||
'/users/logout/',
|
'/users/logout/',
|
||||||
|
@ -121,4 +122,4 @@ def test_api_docs(client: Client):
|
||||||
'scheme': 'basic',
|
'scheme': 'basic',
|
||||||
'name': 'Authorization'
|
'name': 'Authorization'
|
||||||
}
|
}
|
||||||
assert len(docs['definitions']) == 121
|
assert len(docs['definitions']) == 125
|
||||||
|
|
Reference in New Issue