Merge pull request #170 from eReuse/feature/delete-devices

Feature/delete devices
This commit is contained in:
cayop 2021-11-22 12:38:00 +01:00 committed by GitHub
commit b117a4b267
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 196 additions and 74 deletions

View File

@ -12,6 +12,7 @@ ml).
[1.0.10-beta] [1.0.10-beta]
## [1.0.10-beta] ## [1.0.10-beta]
- [addend] #170 can delete/deactivate devices.
- [bugfix] #168 can to do a trade without devices. - [bugfix] #168 can to do a trade without devices.
- [added] #167 new actions of status devices: use, recycling, refurbish and management. - [added] #167 new actions of status devices: use, recycling, refurbish and management.
- [changes] #177 new structure of trade. - [changes] #177 new structure of trade.

View File

@ -0,0 +1,43 @@
"""adding active in device
Revision ID: 8571fb32c912
Revises: 968b79fa7756
Create Date: 2021-10-05 12:27:09.685227
"""
from alembic import op, context
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8571fb32c912'
down_revision = '968b79fa7756'
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_data():
con = op.get_bind()
sql = f"update {get_inv()}.device set active='t';"
con.execute(sql)
def upgrade():
op.add_column('device', sa.Column('active', sa.Boolean(),
default=True,
nullable=True),
schema=f'{get_inv()}')
upgrade_data()
op.alter_column('device', 'active', nullable=False, schema=f'{get_inv()}')
def downgrade():
op.drop_column('device', 'active', schema=f'{get_inv()}')

View File

@ -275,6 +275,11 @@ class MakeAvailable(ActionDef):
SCHEMA = schemas.MakeAvailable SCHEMA = schemas.MakeAvailable
class Delete(ActionDef):
VIEW = None
SCHEMA = schemas.Delete
class ConfirmDef(ActionDef): class ConfirmDef(ActionDef):
VIEW = None VIEW = None
SCHEMA = schemas.Confirm SCHEMA = schemas.Confirm

View File

@ -1773,6 +1773,14 @@ class MoveOnDocument(JoinedTableMixin, ActionWithMultipleTradeDocuments):
container_to_id.comment = """This is the trade document used as container in a outgoing lot""" container_to_id.comment = """This is the trade document used as container in a outgoing lot"""
class Delete(ActionWithMultipleDevices):
# TODO in a new architecture we need rename this class to Deactivate
"""The act save in device who and why this devices was delete.
We never delete one device, but we can deactivate."""
pass
class Migrate(JoinedTableMixin, ActionWithMultipleDevices): class Migrate(JoinedTableMixin, ActionWithMultipleDevices):
"""Moves the devices to a new database/inventory. Devices cannot be """Moves the devices to a new database/inventory. Devices cannot be
modified anymore at the previous database. modified anymore at the previous database.

View File

@ -79,6 +79,15 @@ class ActionWithMultipleDevices(Action):
collection_class=OrderedSet) collection_class=OrderedSet)
class ActionWithMultipleDevicesCheckingOwner(ActionWithMultipleDevices):
@post_load
def check_owner_of_device(self, data):
for dev in data['devices']:
if dev.owner != g.user:
raise ValidationError("Some Devices not exist")
class Add(ActionWithOneDevice): class Add(ActionWithOneDevice):
__doc__ = m.Add.__doc__ __doc__ = m.Add.__doc__
@ -87,7 +96,7 @@ class Remove(ActionWithOneDevice):
__doc__ = m.Remove.__doc__ __doc__ = m.Remove.__doc__
class Allocate(ActionWithMultipleDevices): class Allocate(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Allocate.__doc__ __doc__ = m.Allocate.__doc__
start_time = DateTime(data_key='startTime', required=True, start_time = DateTime(data_key='startTime', required=True,
description=m.Action.start_time.comment) description=m.Action.start_time.comment)
@ -121,7 +130,7 @@ class Allocate(ActionWithMultipleDevices):
device.allocated = True device.allocated = True
class Deallocate(ActionWithMultipleDevices): class Deallocate(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Deallocate.__doc__ __doc__ = m.Deallocate.__doc__
start_time = DateTime(data_key='startTime', required=True, start_time = DateTime(data_key='startTime', required=True,
description=m.Action.start_time.comment) description=m.Action.start_time.comment)
@ -412,15 +421,15 @@ class Snapshot(ActionWithOneDevice):
field_names=['elapsed']) field_names=['elapsed'])
class ToRepair(ActionWithMultipleDevices): class ToRepair(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.ToRepair.__doc__ __doc__ = m.ToRepair.__doc__
class Repair(ActionWithMultipleDevices): class Repair(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Repair.__doc__ __doc__ = m.Repair.__doc__
class Ready(ActionWithMultipleDevices): class Ready(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Ready.__doc__ __doc__ = m.Ready.__doc__
@ -472,15 +481,15 @@ class Management(ActionStatus):
__doc__ = m.Management.__doc__ __doc__ = m.Management.__doc__
class ToPrepare(ActionWithMultipleDevices): class ToPrepare(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.ToPrepare.__doc__ __doc__ = m.ToPrepare.__doc__
class Prepare(ActionWithMultipleDevices): class Prepare(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Prepare.__doc__ __doc__ = m.Prepare.__doc__
class DataWipe(ActionWithMultipleDevices): class DataWipe(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.DataWipe.__doc__ __doc__ = m.DataWipe.__doc__
document = NestedOn(s_generic_document.DataWipeDocument, only_query='id') document = NestedOn(s_generic_document.DataWipeDocument, only_query='id')
@ -530,7 +539,7 @@ class Confirm(ActionWithMultipleDevices):
def validate_revoke(self, data: dict): def validate_revoke(self, data: dict):
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 dev not in data['action'].devices:
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)
@ -543,13 +552,13 @@ class Revoke(ActionWithMultipleDevices):
def validate_revoke(self, data: dict): def validate_revoke(self, data: dict):
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 dev not in data['action'].devices:
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', []): for doc in data.get('documents', []):
# if document not exist in the Trade, then this query is wrong # if document not exist in the Trade, then this query is wrong
if not doc in data['action'].documents: if doc not in data['action'].documents:
txt = "Document {} not exist in the trade".format(doc.file_name) txt = "Document {} not exist in the trade".format(doc.file_name)
raise ValidationError(txt) raise ValidationError(txt)
@ -637,7 +646,7 @@ class RevokeDocument(ActionWithMultipleDocuments):
if not doc.actions: if not doc.actions:
continue continue
if not doc.trading in ['Document Confirmed', 'Confirm']: if doc.trading not in ['Document Confirmed', 'Confirm']:
txt = 'No there are documents to revoke' txt = 'No there are documents to revoke'
raise ValidationError(txt) raise ValidationError(txt)
@ -662,7 +671,6 @@ class ConfirmRevokeDocument(ActionWithMultipleDocuments):
if not doc.actions: if not doc.actions:
continue continue
if not doc.trading == 'Revoke': if not doc.trading == 'Revoke':
txt = 'No there are documents with revoke for confirm' txt = 'No there are documents with revoke for confirm'
raise ValidationError(txt) raise ValidationError(txt)
@ -827,6 +835,16 @@ class TransferOwnershipBlockchain(Trade):
__doc__ = m.TransferOwnershipBlockchain.__doc__ __doc__ = m.TransferOwnershipBlockchain.__doc__
class Delete(ActionWithMultipleDevicesCheckingOwner):
__doc__ = m.Delete.__doc__
@post_load
def deactivate_device(self, data):
for dev in data['devices']:
if dev.last_action_trading is None:
dev.active = False
class Migrate(ActionWithMultipleDevices): class Migrate(ActionWithMultipleDevices):
__doc__ = m.Migrate.__doc__ __doc__ = m.Migrate.__doc__
other = URL() other = URL()

View File

@ -127,6 +127,7 @@ class Device(Thing):
allocated.comment = "device is allocated or not." allocated.comment = "device is allocated or not."
devicehub_id = db.Column(db.CIText(), nullable=True, unique=True, default=create_code) devicehub_id = db.Column(db.CIText(), nullable=True, unique=True, default=create_code)
devicehub_id.comment = "device have a unique code." devicehub_id.comment = "device have a unique code."
active = db.Column(Boolean, default=True)
_NON_PHYSICAL_PROPS = { _NON_PHYSICAL_PROPS = {
'id', 'id',
@ -150,7 +151,8 @@ class Device(Thing):
'sku', 'sku',
'image', 'image',
'allocated', 'allocated',
'devicehub_id' 'devicehub_id',
'active'
} }
__table_args__ = ( __table_args__ = (

View File

@ -154,7 +154,7 @@ class Sync:
db_device = None db_device = None
if device.hid: if device.hid:
with suppress(ResourceNotFound): with suppress(ResourceNotFound):
db_device = Device.query.filter_by(hid=device.hid, owner_id=g.user.id).one() db_device = Device.query.filter_by(hid=device.hid, owner_id=g.user.id, active=True).one()
if db_device and db_device.allocated: if db_device and db_device.allocated:
raise ResourceNotFound('device is actually allocated {}'.format(device)) raise ResourceNotFound('device is actually allocated {}'.format(device))
try: try:

View File

@ -104,7 +104,7 @@ class DeviceView(View):
return super().get(id) return super().get(id)
def patch(self, id): def patch(self, id):
dev = Device.query.filter_by(id=id, owner_id=g.user.id).one() dev = Device.query.filter_by(id=id, owner_id=g.user.id, active=True).one()
if isinstance(dev, Computer): if isinstance(dev, Computer):
resource_def = app.resources['Computer'] resource_def = app.resources['Computer']
# TODO check how to handle the 'actions_one' # TODO check how to handle the 'actions_one'
@ -129,12 +129,12 @@ class DeviceView(View):
return self.one_private(id) return self.one_private(id)
def one_public(self, id: int): def one_public(self, id: int):
device = Device.query.filter_by(devicehub_id=id).one() device = Device.query.filter_by(devicehub_id=id, active=True).one()
return render_template('devices/layout.html', device=device, states=states) return render_template('devices/layout.html', device=device, states=states)
@auth.Auth.requires_auth @auth.Auth.requires_auth
def one_private(self, id: str): def one_private(self, id: str):
device = Device.query.filter_by(devicehub_id=id, owner_id=g.user.id).first() device = Device.query.filter_by(devicehub_id=id, owner_id=g.user.id, active=True).first()
if not device: if not device:
return self.one_public(id) return self.one_public(id)
return self.schema.jsonify(device) return self.schema.jsonify(device)
@ -158,7 +158,7 @@ class DeviceView(View):
trades_dev_ids = {d.id for t in trades for d in t.devices} trades_dev_ids = {d.id for t in trades for d in t.devices}
query = Device.query.filter( query = Device.query.filter(Device.active == True).filter(
(Device.owner_id == g.user.id) | (Device.id.in_(trades_dev_ids)) (Device.owner_id == g.user.id) | (Device.id.in_(trades_dev_ids))
).distinct() ).distinct()

View File

@ -0,0 +1,29 @@
device:
manufacturer: p1
serialNumber: p1
model: p1
type: Desktop
chassis: Tower
components:
- manufacturer: p1c1m
serialNumber: p1c1s
type: Motherboard
- manufacturer: p1c2m
serialNumber: p1c2s
model: p1c2
speed: 1.23
cores: 2
type: Processor
actions:
- type: BenchmarkProcessor
rate: 1
elapsed: 166
- manufacturer: p1c3m
serialNumber: p1c3s
type: GraphicCard
memory: 1.5
elapsed: 25
software: Workbench
uuid: 77860eca-c3fd-41f6-a801-6af7bd8cf832
version: '11.0'
type: Snapshot

View File

@ -2820,6 +2820,75 @@ def test_moveOnDocument(user: UserClient, user2: UserClient):
user.post(res=models.Action, data=request_moveOn, status=422) user.post(res=models.Action, data=request_moveOn, status=422)
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_delete_devices(user: UserClient):
"""This action deactive one device and simulate than one devices is delete."""
snap, _ = user.post(file('acer.happy.battery.snapshot'), res=models.Snapshot)
request = {'type': 'Delete', 'devices': [snap['device']['id']], 'name': 'borrado universal', 'severity': 'Info', 'description': 'duplicity of devices', 'endTime': '2021-07-07T22:00:00.000Z'}
action, _ = user.post(res=models.Action, data=request)
# Check get one device
user.get(res=Device, item=snap['device']['devicehubID'], status=404)
db_device = Device.query.filter_by(id=snap['device']['id']).one()
action_delete = sorted(db_device.actions, key=lambda x: x.created)[-1]
assert action_delete.t == 'Delete'
assert str(action_delete.id) == action['id']
assert db_device.active == False
# Check use of filter from frontend
url = '/devices/?filter={"type":["Computer"]}'
devices, res = user.get(url, None)
assert len(devices['items']) == 0
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_delete_devices_check_sync(user: UserClient):
"""This action deactive one device and simulate than one devices is delete."""
file_snap1 = file('1-device-with-components.snapshot')
file_snap2 = file('2-device-with-components.snapshot')
snap, _ = user.post(file_snap1, res=models.Snapshot)
request = {'type': 'Delete', 'devices': [snap['device']['id']], 'name': 'borrado universal', 'severity': 'Info', 'description': 'duplicity of devices', 'endTime': '2021-07-07T22:00:00.000Z'}
action, _ = user.post(res=models.Action, data=request)
device1 = Device.query.filter_by(id=snap['device']['id']).one()
snap2, _ = user.post(file_snap2, res=models.Snapshot)
request2 = {'type': 'Delete', 'devices': [snap2['device']['id']], 'name': 'borrado universal', 'severity': 'Info', 'description': 'duplicity of devices', 'endTime': '2021-07-07T22:00:00.000Z'}
action2, _ = user.post(res=models.Action, data=request2)
device2 = Device.query.filter_by(id=snap2['device']['id']).one()
# check than device2 is an other device than device1
assert device2.id != device1.id
# check than device2 have the components of device1
assert len([x for x in device2.components
if device1.id in [y.device.id for y in x.actions if hasattr(y, 'device')]]) == 1
@pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_delete_devices_permitions(user: UserClient, user2: UserClient):
"""This action deactive one device and simulate than one devices is delete."""
file_snap = file('1-device-with-components.snapshot')
snap, _ = user.post(file_snap, res=models.Snapshot)
device = Device.query.filter_by(id=snap['device']['id']).one()
request = {'type': 'Delete', 'devices': [snap['device']['id']], 'name': 'borrado universal', 'severity': 'Info', 'description': 'duplicity of devices', 'endTime': '2021-07-07T22:00:00.000Z'}
action, _ = user2.post(res=models.Action, data=request, status=422)
@pytest.mark.mvp @pytest.mark.mvp
@pytest.mark.usefixtures(conftest.app_context.__name__) @pytest.mark.usefixtures(conftest.app_context.__name__)
def test_moveOnDocument_bug168(user: UserClient, user2: UserClient): def test_moveOnDocument_bug168(user: UserClient, user2: UserClient):

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']) == 131 assert len(docs['definitions']) == 132