Merge pull request #80 from eReuse/bugfix/79-manual-merge

Bugfix/79 manual merge
This commit is contained in:
cayop 2020-11-16 20:46:36 +01:00 committed by GitHub
commit 1a7c58f006
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 207 additions and 95 deletions

View file

@ -1 +1 @@
__version__ = "1.0b"
__version__ = "1.0.1-beta"

View 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

View file

@ -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):

View file

@ -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):

View file

@ -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
View 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