Merge pull request #80 from eReuse/bugfix/79-manual-merge
Bugfix/79 manual merge
This commit is contained in:
commit
1a7c58f006
|
@ -1 +1 @@
|
|||
__version__ = "1.0b"
|
||||
__version__ = "1.0.1-beta"
|
||||
|
|
8
ereuse_devicehub/resources/CHANGELOG.md
Normal file
8
ereuse_devicehub/resources/CHANGELOG.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.1-beta] - 2020-11-16
|
||||
- [fixed] #80 manual merged from website
|
|
@ -27,11 +27,13 @@ class DeviceDef(Resource):
|
|||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
|
||||
device_merge = DeviceMergeView.as_view('merge-devices', definition=self, auth=app.auth)
|
||||
|
||||
if self.AUTH:
|
||||
device_merge = app.auth.requires_auth(device_merge)
|
||||
self.add_url_rule('/<{}:{}>/merge/'.format(self.ID_CONVERTER.value, self.ID_NAME),
|
||||
view_func=device_merge,
|
||||
methods={'POST'})
|
||||
|
||||
path = '/<{value}:dev1_id>/merge/<{value}:dev2_id>'.format(value=self.ID_CONVERTER.value)
|
||||
|
||||
self.add_url_rule(path, view_func=device_merge, methods={'POST'})
|
||||
|
||||
|
||||
class ComputerDef(DeviceDef):
|
||||
|
|
|
@ -6,10 +6,13 @@ import marshmallow
|
|||
from flask import g, current_app as app, render_template, request, Response
|
||||
from flask.json import jsonify
|
||||
from flask_sqlalchemy import Pagination
|
||||
from sqlalchemy.util import OrderedSet
|
||||
from marshmallow import fields, fields as f, validate as v, Schema as MarshmallowSchema
|
||||
from teal import query
|
||||
from teal.db import ResourceNotFound
|
||||
from teal.cache import cache
|
||||
from teal.resource import View
|
||||
from teal.marshmallow import ValidationError
|
||||
|
||||
from ereuse_devicehub import auth
|
||||
from ereuse_devicehub.db import db
|
||||
|
@ -170,66 +173,78 @@ class DeviceView(View):
|
|||
|
||||
class DeviceMergeView(View):
|
||||
"""View for merging two devices
|
||||
Ex. ``device/<id>/merge/id=X``.
|
||||
Ex. ``device/<dev1_id>/merge/<dev2_id>``.
|
||||
"""
|
||||
|
||||
class FindArgs(MarshmallowSchema):
|
||||
id = fields.Integer()
|
||||
def post(self, dev1_id: int, dev2_id: int):
|
||||
device = self.merge_devices(dev1_id, dev2_id)
|
||||
|
||||
def get_merge_id(self) -> uuid.UUID:
|
||||
args = self.QUERY_PARSER.parse(self.find_args, request, locations=('querystring',))
|
||||
return args['id']
|
||||
|
||||
def post(self, id: uuid.UUID):
|
||||
device = Device.query.filter_by(id=id).one()
|
||||
with_device = Device.query.filter_by(id=self.get_merge_id()).one()
|
||||
self.merge_devices(device, with_device)
|
||||
|
||||
db.session().final_flush()
|
||||
ret = self.schema.jsonify(device)
|
||||
ret.status_code = 201
|
||||
|
||||
db.session.commit()
|
||||
return ret
|
||||
|
||||
def merge_devices(self, base_device, with_device):
|
||||
"""Merge the current device with `with_device` by
|
||||
adding all `with_device` actions under the current device.
|
||||
@auth.Auth.requires_auth
|
||||
def merge_devices(self, dev1_id: int, dev2_id: int) -> Device:
|
||||
"""Merge the current device with `with_device` (dev2_id) by
|
||||
adding all `with_device` actions under the current device, (dev1_id).
|
||||
|
||||
This operation is highly costly as it forces refreshing
|
||||
many models in session.
|
||||
"""
|
||||
snapshots = sorted(
|
||||
filterfalse(lambda x: not isinstance(x, actions.Snapshot), (base_device.actions + with_device.actions)))
|
||||
workbench_snapshots = [s for s in snapshots if
|
||||
s.software == (SnapshotSoftware.Workbench or SnapshotSoftware.WorkbenchAndroid)]
|
||||
latest_snapshot_device = [d for d in (base_device, with_device) if d.id == snapshots[-1].device.id][0]
|
||||
latest_snapshotworkbench_device = \
|
||||
[d for d in (base_device, with_device) if d.id == workbench_snapshots[-1].device.id][0]
|
||||
# Adding actions of with_device
|
||||
with_actions_one = [a for a in with_device.actions if isinstance(a, actions.ActionWithOneDevice)]
|
||||
with_actions_multiple = [a for a in with_device.actions if isinstance(a, actions.ActionWithMultipleDevices)]
|
||||
# base_device = Device.query.filter_by(id=dev1_id, owner_id=g.user.id).one()
|
||||
self.base_device = Device.query.filter_by(id=dev1_id).one()
|
||||
self.with_device = Device.query.filter_by(id=dev2_id).one()
|
||||
|
||||
if not self.base_device.type == self.with_device.type:
|
||||
# Validation than we are speaking of the same kind of devices
|
||||
raise ValidationError('The devices is not the same type.')
|
||||
|
||||
# Adding actions of self.with_device
|
||||
with_actions_one = [a for a in self.with_device.actions
|
||||
if isinstance(a, actions.ActionWithOneDevice)]
|
||||
with_actions_multiple = [a for a in self.with_device.actions
|
||||
if isinstance(a, actions.ActionWithMultipleDevices)]
|
||||
|
||||
# Moving the tags from `with_device` to `base_device`
|
||||
# Union of tags the device had plus the (potentially) new ones
|
||||
self.base_device.tags.update([x for x in self.with_device.tags])
|
||||
self.with_device.tags.clear() # We don't want to add the transient dummy tags
|
||||
db.session.add(self.with_device)
|
||||
|
||||
# Moving the actions from `with_device` to `base_device`
|
||||
for action in with_actions_one:
|
||||
if action.parent:
|
||||
action.parent = base_device
|
||||
action.parent = self.base_device
|
||||
else:
|
||||
base_device.actions_one.add(action)
|
||||
self.base_device.actions_one.add(action)
|
||||
for action in with_actions_multiple:
|
||||
if action.parent:
|
||||
action.parent = base_device
|
||||
action.parent = self.base_device
|
||||
else:
|
||||
base_device.actions_multiple.add(action)
|
||||
self.base_device.actions_multiple.add(action)
|
||||
|
||||
# Keeping the components of latest SnapshotWorkbench
|
||||
base_device.components = latest_snapshotworkbench_device.components
|
||||
# Keeping the components of with_device
|
||||
components = OrderedSet(c for c in self.with_device.components)
|
||||
self.base_device.components = components
|
||||
|
||||
# Properties from latest Snapshot
|
||||
base_device.type = latest_snapshot_device.type
|
||||
base_device.hid = latest_snapshot_device.hid
|
||||
base_device.manufacturer = latest_snapshot_device.manufacturer
|
||||
base_device.model = latest_snapshot_device.model
|
||||
base_device.chassis = latest_snapshot_device.chassis
|
||||
# Properties from with_device
|
||||
self.merge()
|
||||
|
||||
db.session().add(self.base_device)
|
||||
db.session().final_flush()
|
||||
return self.base_device
|
||||
|
||||
def merge(self):
|
||||
"""Copies the physical properties of the base_device to the with_device.
|
||||
This method mutates base_device.
|
||||
"""
|
||||
for field_name, value in self.with_device.physical_properties.items():
|
||||
if value is not None:
|
||||
setattr(self.base_device, field_name, value)
|
||||
|
||||
self.base_device.hid = self.with_device.hid
|
||||
|
||||
|
||||
class ManufacturerView(View):
|
||||
|
|
|
@ -30,76 +30,76 @@ def test_api_docs(client: Client):
|
|||
assert set(docs['paths'].keys()) == {
|
||||
'/actions/',
|
||||
'/apidocs',
|
||||
'/batteries/{id}/merge/',
|
||||
'/bikes/{id}/merge/',
|
||||
'/cameras/{id}/merge/',
|
||||
'/cellphones/{id}/merge/',
|
||||
'/components/{id}/merge/',
|
||||
'/computer-accessories/{id}/merge/',
|
||||
'/computer-monitors/{id}/merge/',
|
||||
'/computers/{id}/merge/',
|
||||
'/cookings/{id}/merge/',
|
||||
'/data-storages/{id}/merge/',
|
||||
'/dehumidifiers/{id}/merge/',
|
||||
'/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}',
|
||||
'/deliverynotes/',
|
||||
'/desktops/{id}/merge/',
|
||||
'/desktops/{dev1_id}/merge/{dev2_id}',
|
||||
'/devices/',
|
||||
'/devices/static/{filename}',
|
||||
'/devices/{id}/merge/',
|
||||
'/displays/{id}/merge/',
|
||||
'/diy-and-gardenings/{id}/merge/',
|
||||
'/devices/{dev1_id}/merge/{dev2_id}',
|
||||
'/displays/{dev1_id}/merge/{dev2_id}',
|
||||
'/diy-and-gardenings/{dev1_id}/merge/{dev2_id}',
|
||||
'/documents/devices/',
|
||||
'/documents/erasures/',
|
||||
'/documents/lots/',
|
||||
'/documents/static/{filename}',
|
||||
'/documents/stock/',
|
||||
'/drills/{id}/merge/',
|
||||
'/graphic-cards/{id}/merge/',
|
||||
'/hard-drives/{id}/merge/',
|
||||
'/homes/{id}/merge/',
|
||||
'/hubs/{id}/merge/',
|
||||
'/keyboards/{id}/merge/',
|
||||
'/label-printers/{id}/merge/',
|
||||
'/laptops/{id}/merge/',
|
||||
'/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}',
|
||||
'/lots/',
|
||||
'/lots/{id}/children',
|
||||
'/lots/{id}/devices',
|
||||
'/manufacturers/',
|
||||
'/memory-card-readers/{id}/merge/',
|
||||
'/mice/{id}/merge/',
|
||||
'/microphones/{id}/merge/',
|
||||
'/mixers/{id}/merge/',
|
||||
'/mobiles/{id}/merge/',
|
||||
'/monitors/{id}/merge/',
|
||||
'/motherboards/{id}/merge/',
|
||||
'/network-adapters/{id}/merge/',
|
||||
'/networkings/{id}/merge/',
|
||||
'/pack-of-screwdrivers/{id}/merge/',
|
||||
'/printers/{id}/merge/',
|
||||
'/processors/{id}/merge/',
|
||||
'/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}',
|
||||
'/proofs/',
|
||||
'/rackets/{id}/merge/',
|
||||
'/ram-modules/{id}/merge/',
|
||||
'/recreations/{id}/merge/',
|
||||
'/routers/{id}/merge/',
|
||||
'/sais/{id}/merge/',
|
||||
'/servers/{id}/merge/',
|
||||
'/smartphones/{id}/merge/',
|
||||
'/solid-state-drives/{id}/merge/',
|
||||
'/sound-cards/{id}/merge/',
|
||||
'/sounds/{id}/merge/',
|
||||
'/stairs/{id}/merge/',
|
||||
'/switches/{id}/merge/',
|
||||
'/tablets/{id}/merge/',
|
||||
'/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}',
|
||||
'/tags/',
|
||||
'/tags/{tag_id}/device/{device_id}',
|
||||
'/television-sets/{id}/merge/',
|
||||
'/television-sets/{dev1_id}/merge/{dev2_id}',
|
||||
'/users/',
|
||||
'/users/login/',
|
||||
'/video-scalers/{id}/merge/',
|
||||
'/videoconferences/{id}/merge/',
|
||||
'/videos/{id}/merge/',
|
||||
'/wireless-access-points/{id}/merge/',
|
||||
'/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}',
|
||||
'/versions/'
|
||||
}
|
||||
assert docs['info'] == {'title': 'Devicehub', 'version': '0.2'}
|
||||
|
|
87
tests/test_merge.py
Normal file
87
tests/test_merge.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
import datetime
|
||||
from uuid import UUID
|
||||
from flask import g
|
||||
|
||||
import pytest
|
||||
from ereuse_devicehub.client import Client, UserClient
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.resources.action import models as m
|
||||
from ereuse_devicehub.resources.device import models as d
|
||||
from ereuse_devicehub.resources.tag import Tag
|
||||
from tests import conftest
|
||||
from tests.conftest import file as import_snap
|
||||
|
||||
|
||||
@pytest.mark.mvp
|
||||
def test_simple_merge(app: Devicehub, user: UserClient):
|
||||
""" Check if is correct to do a manual merge """
|
||||
snapshot1, _ = user.post(import_snap('real-custom.snapshot.11'), res=m.Snapshot)
|
||||
snapshot2, _ = user.post(import_snap('real-hp.snapshot.11'), res=m.Snapshot)
|
||||
pc1_id = snapshot1['device']['id']
|
||||
pc2_id = snapshot2['device']['id']
|
||||
|
||||
with app.app_context():
|
||||
pc1 = d.Device.query.filter_by(id=pc1_id).one()
|
||||
pc2 = d.Device.query.filter_by(id=pc2_id).one()
|
||||
n_actions1 = len(pc1.actions)
|
||||
n_actions2 = len(pc2.actions)
|
||||
action1 = pc1.actions[0]
|
||||
action2 = pc2.actions[0]
|
||||
assert not action2 in pc1.actions
|
||||
|
||||
tag = Tag(id='foo-bar', owner_id=user.user['id'])
|
||||
pc2.tags.add(tag)
|
||||
db.session.add(pc2)
|
||||
db.session.commit()
|
||||
|
||||
components1 = [com for com in pc1.components]
|
||||
components2 = [com for com in pc2.components]
|
||||
components1_excluded = [com for com in pc1.components if not com in components2]
|
||||
assert pc1.hid != pc2.hid
|
||||
assert not tag in pc1.tags
|
||||
|
||||
uri = '/devices/%d/merge/%d' % (pc1_id, pc2_id)
|
||||
result, _ = user.post({'id': 1}, uri=uri, status=201)
|
||||
|
||||
assert pc1.hid == pc2.hid
|
||||
assert action1 in pc1.actions
|
||||
assert action2 in pc1.actions
|
||||
assert len(pc1.actions) == n_actions1 + n_actions2
|
||||
assert set(pc2.components) == set()
|
||||
assert tag in pc1.tags
|
||||
assert not tag in pc2.tags
|
||||
|
||||
for com in components2:
|
||||
assert com in pc1.components
|
||||
|
||||
for com in components1_excluded:
|
||||
assert not com in pc1.components
|
||||
|
||||
@pytest.mark.mvp
|
||||
def test_merge_two_device_with_differents_tags(app: Devicehub, user: UserClient):
|
||||
""" Check if is correct to do a manual merge of 2 diferents devices with diferents tags """
|
||||
snapshot1, _ = user.post(import_snap('real-custom.snapshot.11'), res=m.Snapshot)
|
||||
snapshot2, _ = user.post(import_snap('real-hp.snapshot.11'), res=m.Snapshot)
|
||||
pc1_id = snapshot1['device']['id']
|
||||
pc2_id = snapshot2['device']['id']
|
||||
|
||||
with app.app_context():
|
||||
pc1 = d.Device.query.filter_by(id=pc1_id).one()
|
||||
pc2 = d.Device.query.filter_by(id=pc2_id).one()
|
||||
|
||||
tag1 = Tag(id='fii-bor', owner_id=user.user['id'])
|
||||
tag2 = Tag(id='foo-bar', owner_id=user.user['id'])
|
||||
pc1.tags.add(tag1)
|
||||
pc2.tags.add(tag2)
|
||||
db.session.add(pc1)
|
||||
db.session.add(pc2)
|
||||
db.session.commit()
|
||||
|
||||
uri = '/devices/%d/merge/%d' % (pc1_id, pc2_id)
|
||||
result, _ = user.post({'id': 1}, uri=uri, status=201)
|
||||
|
||||
assert pc1.hid == pc2.hid
|
||||
assert tag1 in pc1.tags
|
||||
assert tag2 in pc1.tags
|
||||
|
Reference in a new issue