diff --git a/CHANGELOG.md b/CHANGELOG.md index 016f22d7..e8bb5e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ml). [1.0.10-beta] ## [1.0.10-beta] +- [addend] #170 can delete/deactivate devices. - [bugfix] #168 can to do a trade without devices. - [added] #167 new actions of status devices: use, recycling, refurbish and management. - [changes] #177 new structure of trade. diff --git a/ereuse_devicehub/migrations/versions/8571fb32c912_adding_active_in_device.py b/ereuse_devicehub/migrations/versions/8571fb32c912_adding_active_in_device.py new file mode 100644 index 00000000..79f18385 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/8571fb32c912_adding_active_in_device.py @@ -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()}') diff --git a/ereuse_devicehub/resources/action/__init__.py b/ereuse_devicehub/resources/action/__init__.py index 0d89f557..b405e164 100644 --- a/ereuse_devicehub/resources/action/__init__.py +++ b/ereuse_devicehub/resources/action/__init__.py @@ -275,6 +275,11 @@ class MakeAvailable(ActionDef): SCHEMA = schemas.MakeAvailable +class Delete(ActionDef): + VIEW = None + SCHEMA = schemas.Delete + + class ConfirmDef(ActionDef): VIEW = None SCHEMA = schemas.Confirm diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index e4e8c3b6..93cdcdef 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -1773,6 +1773,14 @@ class MoveOnDocument(JoinedTableMixin, ActionWithMultipleTradeDocuments): 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): """Moves the devices to a new database/inventory. Devices cannot be modified anymore at the previous database. diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index 1f7f3fef..eb69f936 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -79,6 +79,15 @@ class ActionWithMultipleDevices(Action): 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): __doc__ = m.Add.__doc__ @@ -87,7 +96,7 @@ class Remove(ActionWithOneDevice): __doc__ = m.Remove.__doc__ -class Allocate(ActionWithMultipleDevices): +class Allocate(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.Allocate.__doc__ start_time = DateTime(data_key='startTime', required=True, description=m.Action.start_time.comment) @@ -121,7 +130,7 @@ class Allocate(ActionWithMultipleDevices): device.allocated = True -class Deallocate(ActionWithMultipleDevices): +class Deallocate(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.Deallocate.__doc__ start_time = DateTime(data_key='startTime', required=True, description=m.Action.start_time.comment) @@ -412,15 +421,15 @@ class Snapshot(ActionWithOneDevice): field_names=['elapsed']) -class ToRepair(ActionWithMultipleDevices): +class ToRepair(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.ToRepair.__doc__ -class Repair(ActionWithMultipleDevices): +class Repair(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.Repair.__doc__ -class Ready(ActionWithMultipleDevices): +class Ready(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.Ready.__doc__ @@ -472,15 +481,15 @@ class Management(ActionStatus): __doc__ = m.Management.__doc__ -class ToPrepare(ActionWithMultipleDevices): +class ToPrepare(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.ToPrepare.__doc__ -class Prepare(ActionWithMultipleDevices): +class Prepare(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.Prepare.__doc__ -class DataWipe(ActionWithMultipleDevices): +class DataWipe(ActionWithMultipleDevicesCheckingOwner): __doc__ = m.DataWipe.__doc__ document = NestedOn(s_generic_document.DataWipeDocument, only_query='id') @@ -530,7 +539,7 @@ class Confirm(ActionWithMultipleDevices): def validate_revoke(self, data: dict): for dev in data['devices']: # 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) raise ValidationError(txt) @@ -543,13 +552,13 @@ class Revoke(ActionWithMultipleDevices): def validate_revoke(self, data: dict): for dev in data['devices']: # 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) 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: + if doc not in data['action'].documents: txt = "Document {} not exist in the trade".format(doc.file_name) raise ValidationError(txt) @@ -610,7 +619,7 @@ class ConfirmDocument(ActionWithMultipleDocuments): if not doc.actions: continue - if not doc.trading == 'Need Confirmation': + if not doc.trading == 'Need Confirmation': txt = 'No there are documents to confirm' raise ValidationError(txt) @@ -637,7 +646,7 @@ class RevokeDocument(ActionWithMultipleDocuments): if not doc.actions: continue - if not doc.trading in ['Document Confirmed', 'Confirm']: + if doc.trading not in ['Document Confirmed', 'Confirm']: txt = 'No there are documents to revoke' raise ValidationError(txt) @@ -662,7 +671,6 @@ class ConfirmRevokeDocument(ActionWithMultipleDocuments): if not doc.actions: continue - if not doc.trading == 'Revoke': txt = 'No there are documents with revoke for confirm' raise ValidationError(txt) @@ -827,6 +835,16 @@ class TransferOwnershipBlockchain(Trade): __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): __doc__ = m.Migrate.__doc__ other = URL() diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index e7297fcd..a97c7392 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -127,6 +127,7 @@ class Device(Thing): allocated.comment = "device is allocated or not." devicehub_id = db.Column(db.CIText(), nullable=True, unique=True, default=create_code) devicehub_id.comment = "device have a unique code." + active = db.Column(Boolean, default=True) _NON_PHYSICAL_PROPS = { 'id', @@ -150,7 +151,8 @@ class Device(Thing): 'sku', 'image', 'allocated', - 'devicehub_id' + 'devicehub_id', + 'active' } __table_args__ = ( diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index 81c57657..8608ad95 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -154,7 +154,7 @@ class Sync: db_device = None if device.hid: 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: raise ResourceNotFound('device is actually allocated {}'.format(device)) try: diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index b046b49d..8bab2a3c 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -104,7 +104,7 @@ class DeviceView(View): return super().get(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): resource_def = app.resources['Computer'] # TODO check how to handle the 'actions_one' @@ -129,12 +129,12 @@ class DeviceView(View): return self.one_private(id) 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) @auth.Auth.requires_auth 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: return self.one_public(id) 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} - query = Device.query.filter( + query = Device.query.filter(Device.active == True).filter( (Device.owner_id == g.user.id) | (Device.id.in_(trades_dev_ids)) ).distinct() diff --git a/tests/files/2-device-with-components.snapshot.yaml b/tests/files/2-device-with-components.snapshot.yaml new file mode 100644 index 00000000..51d36265 --- /dev/null +++ b/tests/files/2-device-with-components.snapshot.yaml @@ -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 diff --git a/tests/test_action.py b/tests/test_action.py index 0e339c6d..8de74e81 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -2820,6 +2820,75 @@ def test_moveOnDocument(user: UserClient, user2: UserClient): 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.usefixtures(conftest.app_context.__name__) def test_moveOnDocument_bug168(user: UserClient, user2: UserClient): diff --git a/tests/test_basic.py b/tests/test_basic.py index bdf48b8d..974140b4 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -59,59 +59,6 @@ def test_api_docs(client: Client): '/users/', '/users/login/', '/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['components']['securitySchemes']['bearerAuth'] == { @@ -122,4 +69,4 @@ def test_api_docs(client: Client): 'scheme': 'basic', 'name': 'Authorization' } - assert len(docs['definitions']) == 131 + assert len(docs['definitions']) == 132