resolve conflict

This commit is contained in:
Cayo Puigdefabregas 2021-10-18 11:47:55 +02:00
commit 5ec005e63b
10 changed files with 475 additions and 55 deletions

View File

@ -14,6 +14,7 @@ ml).
## [1.0.10-beta] ## [1.0.10-beta]
- [addend] #170 can delete/deactivate devices. - [addend] #170 can delete/deactivate devices.
- [bugfix] #168 can to do a trade without devices. - [bugfix] #168 can to do a trade without devices.
- [addend] #167 new actions of status devices: use, recycling, refurbish and management.
## [1.0.9-beta] ## [1.0.9-beta]
- [addend] #159 external document as proof of erase of disk - [addend] #159 external document as proof of erase of disk

View File

@ -1 +1 @@
__version__ = "1.0.9-beta" __version__ = "1.0.10-beta"

View File

@ -0,0 +1,38 @@
"""adding state actions
Revision ID: a0978ac6cf4a
Revises: 7ecb8ff7abad
Create Date: 2021-09-24 12:03:01.661679
"""
from alembic import op, context
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'a0978ac6cf4a'
down_revision = '3ac2bc1897ce'
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('action_status',
sa.Column('rol_user_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.ForeignKeyConstraint(['id'], [f'{get_inv()}.action.id'], ),
sa.ForeignKeyConstraint(['rol_user_id'], ['common.user.id'], ),
sa.PrimaryKeyConstraint('id'),
schema=f'{get_inv()}'
)
def downgrade():
op.drop_table('action_status', schema=f'{get_inv()}')

View File

@ -194,6 +194,26 @@ class ReadyDef(ActionDef):
SCHEMA = schemas.Ready SCHEMA = schemas.Ready
class RecyclingDef(ActionDef):
VIEW = None
SCHEMA = schemas.Recycling
class UseDef(ActionDef):
VIEW = None
SCHEMA = schemas.Use
class RefurbishDef(ActionDef):
VIEW = None
SCHEMA = schemas.Refurbish
class ManagementDef(ActionDef):
VIEW = None
SCHEMA = schemas.Management
class ToPrepareDef(ActionDef): class ToPrepareDef(ActionDef):
VIEW = None VIEW = None
SCHEMA = schemas.ToPrepare SCHEMA = schemas.ToPrepare

View File

@ -1350,6 +1350,33 @@ class DataWipe(JoinedTableMixin, ActionWithMultipleDevices):
primaryjoin='DataWipe.document_id == DataWipeDocument.id') primaryjoin='DataWipe.document_id == DataWipeDocument.id')
class ActionStatus(JoinedTableMixin, ActionWithMultipleTradeDocuments):
"""This is a meta-action than mark the status of the devices"""
rol_user_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
rol_user = db.relationship(User, primaryjoin=rol_user_id == User.id)
rol_user_comment = """The user that ."""
class Recycling(ActionStatus):
"""This action mark devices as recycling"""
class Use(ActionStatus):
"""This action mark one devices or container as use"""
class Refurbish(ActionStatus):
"""This action mark one devices or container as refurbish"""
class Management(ActionStatus):
"""This action mark one devices or container as management"""
class Prepare(ActionWithMultipleDevices): class Prepare(ActionWithMultipleDevices):
"""Work has been performed to the device to a defined point of """Work has been performed to the device to a defined point of
acceptance. acceptance.
@ -1471,6 +1498,20 @@ class CancelReservation(Organize):
"""The act of cancelling a reservation.""" """The act of cancelling a reservation."""
class ActionStatusDocuments(JoinedTableMixin, ActionWithMultipleTradeDocuments):
"""This is a meta-action that marks the state of the devices."""
rol_user_id = db.Column(UUID(as_uuid=True),
db.ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
rol_user = db.relationship(User, primaryjoin=rol_user_id == User.id)
rol_user_comment = """The user that ."""
class RecyclingDocument(ActionStatusDocuments):
"""This action mark one document or container as recycling"""
class ConfirmDocument(JoinedTableMixin, ActionWithMultipleTradeDocuments): class ConfirmDocument(JoinedTableMixin, ActionWithMultipleTradeDocuments):
"""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 document that confirm and the document that confirm

View File

@ -424,6 +424,50 @@ class Ready(ActionWithMultipleDevices):
__doc__ = m.Ready.__doc__ __doc__ = m.Ready.__doc__
class ActionStatus(Action):
rol_user = NestedOn(s_user.User, dump_only=True, exclude=('token',))
devices = NestedOn(s_device.Device,
many=True,
required=False, # todo test ensuring len(devices) >= 1
only_query='id',
collection_class=OrderedSet)
documents = NestedOn(s_document.TradeDocument,
many=True,
required=False, # todo test ensuring len(devices) >= 1
only_query='id',
collection_class=OrderedSet)
@pre_load
def put_devices(self, data: dict):
if not 'devices' in data.keys():
data['devices'] = []
@post_load
def put_rol_user(self, data: dict):
for dev in data['devices']:
if dev.trading in [None, 'Revoke', 'ConfirmRevoke']:
return data
trade = [ac for ac in dev.actions if ac.t == 'Trade'][-1]
if trade.user_to != g.user:
data['rol_user'] = trade.user_to
class Recycling(ActionStatus):
__doc__ = m.Recycling.__doc__
class Use(ActionStatus):
__doc__ = m.Use.__doc__
class Refurbish(ActionStatus):
__doc__ = m.Refurbish.__doc__
class Management(ActionStatus):
__doc__ = m.Management.__doc__
class ToPrepare(ActionWithMultipleDevices): class ToPrepare(ActionWithMultipleDevices):
__doc__ = m.ToPrepare.__doc__ __doc__ = m.ToPrepare.__doc__

View File

@ -263,6 +263,46 @@ class Device(Thing):
with suppress(LookupError, ValueError): with suppress(LookupError, ValueError):
return self.last_action_of(*states.Trading.actions()) return self.last_action_of(*states.Trading.actions())
@property
def status(self):
"""Show the actual status of device for this owner.
The status depend of one of this 4 actions:
- Use
- Refurbish
- Recycling
- Management
"""
from ereuse_devicehub.resources.device import states
with suppress(LookupError, ValueError):
return self.last_action_of(*states.Status.actions())
@property
def history_status(self):
"""Show the history of the status actions of the device.
The status depend of one of this 4 actions:
- Use
- Refurbish
- Recycling
- Management
"""
from ereuse_devicehub.resources.device import states
status_actions = [ac.t for ac in states.Status.actions()]
history = []
for ac in self.actions:
if not ac.t in status_actions:
continue
if not history:
history.append(ac)
continue
if ac.rol_user == history[-1].rol_user:
# get only the last action consecutive for the same user
history = history[:-1] + [ac]
continue
history.append(ac)
return history
@property @property
def trading(self): def trading(self):
"""The trading state, or None if no Trade action has """The trading state, or None if no Trade action has

View File

@ -83,3 +83,16 @@ class Usage(State):
Allocate = e.Allocate Allocate = e.Allocate
Deallocate = e.Deallocate Deallocate = e.Deallocate
InUse = e.Live InUse = e.Live
class Status(State):
"""Define status of device for one user.
:cvar Use: The device is in use for one final user.
:cvar Refurbish: The device is owned by one refurbisher.
:cvar Recycling: The device is sended to recycling.
:cvar Management: The device is owned by one Manager.
"""
Use = e.Use
Refurbish = e.Refurbish
Recycling = e.Recycling
Management = e.Management

View File

@ -256,6 +256,282 @@ def test_generic_action(action_model_state: Tuple[models.Action, states.Trading]
assert snapshot['device']['updated'] != device['updated'] assert snapshot['device']['updated'] != device['updated']
@pytest.mark.mvp
@pytest.mark.parametrize('action_model',
(pytest.param(ams, id=ams.__class__.__name__)
for ams in [
models.Recycling,
models.Use,
models.Refurbish,
models.Management
]))
def test_simple_status_actions(action_model: models.Action, user: UserClient, user2: UserClient):
"""Simple test of status action."""
snap, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
action = {'type': action_model.t, 'devices': [snap['device']['id']]}
action, _ = user.post(action, res=models.Action)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
assert device['actions'][-1]['id'] == action['id']
assert action['author']['id'] == user.user['id']
assert action['rol_user']['id'] == user.user['id']
@pytest.mark.mvp
@pytest.mark.parametrize('action_model',
(pytest.param(ams, id=ams.__class__.__name__)
for ams in [
models.Recycling,
models.Use,
models.Refurbish,
models.Management
]))
def test_outgoinlot_status_actions(action_model: models.Action, user: UserClient, user2: UserClient):
"""Test of status actions in outgoinlot."""
snap, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
lot, _ = user.post({'name': 'MyLot'}, res=Lot)
user.post({},
res=Lot,
item='{}/devices'.format(lot['id']),
query=[('id', device['id'])])
request_post = {
'type': 'Trade',
'devices': [device['id']],
'userFromEmail': user.email,
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'lot': lot['id'],
'confirms': True,
}
user.post(res=models.Action, data=request_post)
action = {'type': action_model.t, 'devices': [device['id']]}
action, _ = user.post(action, res=models.Action)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
assert device['actions'][-1]['id'] == action['id']
assert action['author']['id'] == user.user['id']
assert action['rol_user']['id'] == user2.user['id']
# Remove device from lot
lot, _ = user.delete({},
res=Lot,
item='{}/devices'.format(lot['id']),
query=[('id', device['id'])], status=200)
action = {'type': action_model.t, 'devices': [device['id']]}
action, _ = user.post(action, res=models.Action)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
assert device['actions'][-1]['id'] == action['id']
assert action['author']['id'] == user.user['id']
assert action['rol_user']['id'] == user.user['id']
@pytest.mark.mvp
@pytest.mark.parametrize('action_model',
(pytest.param(ams, id=ams.__class__.__name__)
for ams in [
models.Recycling,
models.Use,
models.Refurbish,
models.Management
]))
def test_incominglot_status_actions(action_model: models.Action, user: UserClient, user2: UserClient):
"""Test of status actions in outgoinlot."""
snap, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
lot, _ = user.post({'name': 'MyLot'}, res=Lot)
user.post({},
res=Lot,
item='{}/devices'.format(lot['id']),
query=[('id', device['id'])])
request_post = {
'type': 'Trade',
'devices': [device['id']],
'userFromEmail': user2.email,
'userToEmail': user.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'lot': lot['id'],
'confirms': True,
}
user.post(res=models.Action, data=request_post)
action = {'type': action_model.t, 'devices': [device['id']]}
action, _ = user.post(action, res=models.Action)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
assert device['actions'][-1]['id'] == action['id']
assert action['author']['id'] == user.user['id']
assert action['rol_user']['id'] == user.user['id']
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_history_status_actions(user: UserClient, user2: UserClient):
"""Test for check the status actions."""
snap, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
device = Device.query.filter_by(id=snap['device']['id']).one()
# Case 1
action = {'type': models.Recycling.t, 'devices': [device.id]}
action, _ = user.post(action, res=models.Action)
assert str(device.actions[-1].id) == action['id']
assert action['id'] == str(device.status.id)
assert device.status.t == models.Recycling.t
assert [action['id']] == [str(ac.id) for ac in device.history_status]
# Case 2
action2 = {'type': models.Refurbish.t, 'devices': [device.id]}
action2, _ = user.post(action2, res=models.Action)
assert action2['id'] == str(device.status.id)
assert device.status.t == models.Refurbish.t
assert [action2['id']] == [str(ac.id) for ac in device.history_status]
# Case 3
lot, _ = user.post({'name': 'MyLot'}, res=Lot)
user.post({},
res=Lot,
item='{}/devices'.format(lot['id']),
query=[('id', device.id)])
request_post = {
'type': 'Trade',
'devices': [device.id],
'userFromEmail': user.email,
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'lot': lot['id'],
'confirms': True,
}
user.post(res=models.Action, data=request_post)
action3 = {'type': models.Use.t, 'devices': [device.id]}
action3, _ = user.post(action3, res=models.Action)
assert action3['id'] == str(device.status.id)
assert device.status.t == models.Use.t
assert [action2['id'], action3['id']] == [str(ac.id) for ac in device.history_status]
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_use_changing_owner(user: UserClient, user2: UserClient):
"""Check if is it possible to do a use action for one device
when you are not the owner.
"""
snap, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
device = Device.query.filter_by(id=snap['device']['id']).one()
assert device.owner.email == user.email
# Trade
lot, _ = user.post({'name': 'MyLot'}, res=Lot)
user.post({},
res=Lot,
item='{}/devices'.format(lot['id']),
query=[('id', device.id)])
request_post = {
'type': 'Trade',
'devices': [device.id],
'userFromEmail': user.email,
'userToEmail': user2.email,
'price': 10,
'date': "2020-12-01T02:00:00+00:00",
'lot': lot['id'],
'confirms': True,
}
user.post(res=models.Action, data=request_post)
trade = models.Trade.query.one()
# Doble confirmation and change of owner
request_confirm = {
'type': 'Confirm',
'action': trade.id,
'devices': [device.id]
}
user2.post(res=models.Action, data=request_confirm)
assert device.owner.email == user2.email
# Adding action Use
action3 = {'type': models.Use.t, 'devices': [device.id]}
action3, _ = user.post(action3, res=models.Action)
assert action3['id'] == str(device.status.id)
assert device.status.t == models.Use.t
assert device.owner.email == user2.email
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_recycling_container(user: UserClient):
"""Test of status action recycling for a container."""
lot, _ = user.post({'name': 'MyLotOut'}, res=Lot)
url = 'http://www.ereuse.org/',
request_post = {
'filename': 'test.pdf',
'hash': 'bbbbbbbb',
'url': url,
'weight': 150,
'lot': lot['id']
}
tradedocument, _ = user.post(res=TradeDocument, data=request_post)
action = {'type': models.Recycling.t, 'devices': [], 'documents': [tradedocument['id']]}
action, _ = user.post(action, res=models.Action)
trade = TradeDocument.query.one()
assert str(trade.actions[0].id) == action['id']
@pytest.mark.mvp
@pytest.mark.parametrize('action_model',
(pytest.param(ams, id=ams.__class__.__name__)
for ams in [
models.Recycling,
models.Use,
models.Refurbish,
models.Management
]))
def test_status_without_lot(action_model: models.Action, user: UserClient):
"""Test of status actions for devices without lot."""
snap, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
action = {'type': action_model.t, 'devices': [snap['device']['id']]}
action, _ = user.post(action, res=models.Action)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
assert device['actions'][-1]['id'] == action['id']
@pytest.mark.mvp
@pytest.mark.parametrize('action_model',
(pytest.param(ams, id=ams.__class__.__name__)
for ams in [
models.Recycling,
models.Use,
models.Refurbish,
models.Management
]))
def test_status_in_temporary_lot(action_model: models.Action, user: UserClient):
"""Test of status actions for devices in a temporary lot."""
snap, _ = user.post(file('basic.snapshot'), res=models.Snapshot)
device_id = snap['device']['id']
lot, _ = user.post({'name': 'MyLotOut'}, res=Lot)
lot, _ = user.post({},
res=Lot,
item='{}/devices'.format(lot['id']),
query=[('id', device_id)])
action = {'type': action_model.t, 'devices': [device_id]}
action, _ = user.post(action, res=models.Action)
device, _ = user.get(res=Device, item=snap['device']['devicehubID'])
assert device['actions'][-1]['id'] == action['id']
@pytest.mark.mvp @pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__) @pytest.mark.usefixtures(conftest.app_context.__name__)
def test_live(user: UserClient, client: Client, app: Devicehub): def test_live(user: UserClient, client: Client, app: Devicehub):

View File

@ -59,59 +59,6 @@ def test_api_docs(client: Client):
'/users/', '/users/',
'/users/login/', '/users/login/',
'/users/logout/', '/users/logout/',
# '/devices/{dev1_id}/merge/{dev2_id}',
# '/batteries/{dev1_id}/merge/{dev2_id}',
# '/bikes/{dev1_id}/merge/{dev2_id}',
# '/cameras/{dev1_id}/merge/{dev2_id}',
# '/cellphones/{dev1_id}/merge/{dev2_id}',
# '/components/{dev1_id}/merge/{dev2_id}',
# '/computer-accessories/{dev1_id}/merge/{dev2_id}',
# '/computer-monitors/{dev1_id}/merge/{dev2_id}',
# '/computers/{dev1_id}/merge/{dev2_id}',
# '/cookings/{dev1_id}/merge/{dev2_id}',
# '/data-storages/{dev1_id}/merge/{dev2_id}',
# '/dehumidifiers/{dev1_id}/merge/{dev2_id}',
# '/desktops/{dev1_id}/merge/{dev2_id}',
# '/displays/{dev1_id}/merge/{dev2_id}',
# '/diy-and-gardenings/{dev1_id}/merge/{dev2_id}',
# '/drills/{dev1_id}/merge/{dev2_id}',
# '/graphic-cards/{dev1_id}/merge/{dev2_id}',
# '/hard-drives/{dev1_id}/merge/{dev2_id}',
# '/homes/{dev1_id}/merge/{dev2_id}',
# '/hubs/{dev1_id}/merge/{dev2_id}',
# '/keyboards/{dev1_id}/merge/{dev2_id}',
# '/label-printers/{dev1_id}/merge/{dev2_id}',
# '/laptops/{dev1_id}/merge/{dev2_id}',
# '/memory-card-readers/{dev1_id}/merge/{dev2_id}',
# '/mice/{dev1_id}/merge/{dev2_id}',
# '/microphones/{dev1_id}/merge/{dev2_id}',
# '/mixers/{dev1_id}/merge/{dev2_id}',
# '/mobiles/{dev1_id}/merge/{dev2_id}',
# '/monitors/{dev1_id}/merge/{dev2_id}',
# '/motherboards/{dev1_id}/merge/{dev2_id}',
# '/network-adapters/{dev1_id}/merge/{dev2_id}',
# '/networkings/{dev1_id}/merge/{dev2_id}',
# '/pack-of-screwdrivers/{dev1_id}/merge/{dev2_id}',
# '/printers/{dev1_id}/merge/{dev2_id}',
# '/processors/{dev1_id}/merge/{dev2_id}',
# '/rackets/{dev1_id}/merge/{dev2_id}',
# '/ram-modules/{dev1_id}/merge/{dev2_id}',
# '/recreations/{dev1_id}/merge/{dev2_id}',
# '/routers/{dev1_id}/merge/{dev2_id}',
# '/sais/{dev1_id}/merge/{dev2_id}',
# '/servers/{dev1_id}/merge/{dev2_id}',
# '/smartphones/{dev1_id}/merge/{dev2_id}',
# '/solid-state-drives/{dev1_id}/merge/{dev2_id}',
# '/sound-cards/{dev1_id}/merge/{dev2_id}',
# '/sounds/{dev1_id}/merge/{dev2_id}',
# '/stairs/{dev1_id}/merge/{dev2_id}',
# '/switches/{dev1_id}/merge/{dev2_id}',
# '/tablets/{dev1_id}/merge/{dev2_id}',
# '/television-sets/{dev1_id}/merge/{dev2_id}',
# '/video-scalers/{dev1_id}/merge/{dev2_id}',
# '/videoconferences/{dev1_id}/merge/{dev2_id}',
# '/videos/{dev1_id}/merge/{dev2_id}',
# '/wireless-access-points/{dev1_id}/merge/{dev2_id}',
} }
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'} assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
assert docs['components']['securitySchemes']['bearerAuth'] == { assert docs['components']['securitySchemes']['bearerAuth'] == {
@ -122,4 +69,4 @@ def test_api_docs(client: Client):
'scheme': 'basic', 'scheme': 'basic',
'name': 'Authorization' 'name': 'Authorization'
} }
assert len(docs['definitions']) == 128 assert len(docs['definitions']) == 132