Link tags to devices through PUT; update client

This commit is contained in:
Xavier Bustamante Talavera 2018-09-20 11:51:25 +02:00
parent 3b0f483a90
commit 32837f5f59
6 changed files with 135 additions and 31 deletions

View File

@ -1,14 +1,14 @@
from inspect import isclass from inspect import isclass
from typing import Any, Dict, Iterable, Tuple, Type, Union from typing import Dict, Iterable, Type, Union
from ereuse_utils.test import JSON from ereuse_utils.test import JSON, Res
from flask import Response from teal.client import Client as TealClient, Query, Status
from teal.client import Client as TealClient
from teal.marshmallow import ValidationError
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from ereuse_devicehub.resources import models, schemas from ereuse_devicehub.resources import models, schemas
ResourceLike = Union[Type[Union[models.Thing, schemas.Thing]], str]
class Client(TealClient): class Client(TealClient):
"""A client suited for Devicehub main usage.""" """A client suited for Devicehub main usage."""
@ -21,15 +21,15 @@ class Client(TealClient):
def open(self, def open(self,
uri: str, uri: str,
res: Union[str, Type[Union[models.Thing, schemas.Thing]]] = None, res: ResourceLike = None,
status: Union[int, Type[HTTPException], Type[ValidationError]] = 200, status: Status = 200,
query: Iterable[Tuple[str, Any]] = tuple(), query: Query = tuple(),
accept=JSON, accept=JSON,
content_type=JSON, content_type=JSON,
item=None, item=None,
headers: dict = None, headers: dict = None,
token: str = None, token: str = None,
**kw) -> Tuple[Union[Dict[str, object], str], Response]: **kw) -> Res:
if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)): if isclass(res) and issubclass(res, (models.Thing, schemas.Thing)):
res = res.t res = res.t
return super().open(uri, res, status, query, accept, content_type, item, headers, token, return super().open(uri, res, status, query, accept, content_type, item, headers, token,
@ -37,37 +37,79 @@ class Client(TealClient):
def get(self, def get(self,
uri: str = '', uri: str = '',
res: Union[Type[Union[models.Thing, schemas.Thing]], str] = None, res: ResourceLike = None,
query: Iterable[Tuple[str, Any]] = tuple(), query: Query = tuple(),
status: Union[int, Type[HTTPException], Type[ValidationError]] = 200, status: Status = 200,
item: Union[int, str] = None, item: Union[int, str] = None,
accept: str = JSON, accept: str = JSON,
headers: dict = None, headers: dict = None,
token: str = None, token: str = None,
**kw) -> Tuple[Union[Dict[str, object], str], Response]: **kw) -> Res:
return super().get(uri, res, query, status, item, accept, headers, token, **kw) return super().get(uri, res, query, status, item, accept, headers, token, **kw)
def post(self, def post(self,
data: str or dict, data: str or dict,
uri: str = '', uri: str = '',
res: Union[Type[Union[models.Thing, schemas.Thing]], str] = None, res: ResourceLike = None,
query: Iterable[Tuple[str, Any]] = tuple(), query: Query = tuple(),
status: Union[int, Type[HTTPException], Type[ValidationError]] = 201, status: Status = 201,
content_type: str = JSON, content_type: str = JSON,
accept: str = JSON, accept: str = JSON,
headers: dict = None, headers: dict = None,
token: str = None, token: str = None,
**kw) -> Tuple[Union[Dict[str, object], str], Response]: **kw) -> Res:
return super().post(data, uri, res, query, status, content_type, accept, headers, token, return super().post(data, uri, res, query, status, content_type, accept, headers, token,
**kw) **kw)
def patch(self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
item: Union[int, str] = None,
status: Status = 200,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().patch(data, uri, res, query, item, status, content_type, accept, token,
headers, **kw)
def put(self,
data: str or dict,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
item: Union[int, str] = None,
status: Status = 201,
content_type: str = JSON,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().put(data, uri, res, query, item, status, content_type, accept, token,
headers, **kw)
def delete(self,
uri: str = '',
res: ResourceLike = None,
query: Query = tuple(),
status: Status = 204,
item: Union[int, str] = None,
accept: str = JSON,
headers: dict = None,
token: str = None,
**kw) -> Res:
return super().delete(uri, res, query, status, item, accept, headers, token, **kw)
def login(self, email: str, password: str): def login(self, email: str, password: str):
assert isinstance(email, str) assert isinstance(email, str)
assert isinstance(password, str) assert isinstance(password, str)
return self.post({'email': email, 'password': password}, '/users/login', status=200) return self.post({'email': email, 'password': password}, '/users/login', status=200)
def get_many(self, def get_many(self,
res: Union[Type[Union[models.Thing, schemas.Thing]], str], res: ResourceLike,
resources: Iterable[Union[dict, int]], resources: Iterable[Union[dict, int]],
key: str = None, key: str = None,
**kw) -> Iterable[Union[Dict[str, object], str]]: **kw) -> Iterable[Union[Dict[str, object], str]]:
@ -98,18 +140,19 @@ class UserClient(Client):
def open(self, def open(self,
uri: str, uri: str,
res: Union[str, Type[Union[models.Thing, schemas.Thing]]] = None, res: ResourceLike = None,
status: int or HTTPException = 200, status: int or HTTPException = 200,
query: Iterable[Tuple[str, Any]] = tuple(), query: Query = tuple(),
accept=JSON, accept=JSON,
content_type=JSON, content_type=JSON,
item=None, item=None,
headers: dict = None, headers: dict = None,
token: str = None, token: str = None,
**kw) -> Tuple[Union[Dict[str, object], str], Response]: **kw) -> Res:
return super().open(uri, res, status, query, accept, content_type, item, headers, return super().open(uri, res, status, query, accept, content_type, item, headers,
self.user['token'] if self.user else token, **kw) self.user['token'] if self.user else token, **kw)
# noinspection PyMethodOverriding
def login(self): def login(self):
response = super().login(self.email, self.password) response = super().login(self.email, self.password)
self.user = response[0] self.user = response[0]

View File

@ -7,9 +7,10 @@ from teal.resource import Resource
from teal.teal import Teal from teal.teal import Teal
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device import DeviceDef
from ereuse_devicehub.resources.tag import schema from ereuse_devicehub.resources.tag import schema
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.tag.view import TagView, get_device_from_tag from ereuse_devicehub.resources.tag.view import TagDeviceView, TagView, get_device_from_tag
class TagDef(Resource): class TagDef(Resource):
@ -33,10 +34,18 @@ class TagDef(Resource):
super().__init__(app, import_name, static_folder, static_url_path, template_folder, super().__init__(app, import_name, static_folder, static_url_path, template_folder,
url_prefix, subdomain, url_defaults, root_path, cli_commands) url_prefix, subdomain, url_defaults, root_path, cli_commands)
_get_device_from_tag = app.auth.requires_auth(get_device_from_tag) _get_device_from_tag = app.auth.requires_auth(get_device_from_tag)
self.add_url_rule('/<{}:{}>/device'.format(self.ID_CONVERTER.value, self.ID_NAME),
view_func=_get_device_from_tag, # DeviceTagView URLs
device_view = TagDeviceView.as_view('tag-device-view', definition=self, auth=app.auth)
if self.AUTH:
device_view = app.auth.requires_auth(device_view)
self.add_url_rule('/<{0.ID_CONVERTER.value}:{0.ID_NAME}>/device'.format(self),
view_func=device_view,
methods={'GET'}) methods={'GET'})
self.tag_schema = schema.Tag self.add_url_rule('/<{0.ID_CONVERTER.value}:tag_id>/'.format(self) +
'device/<{0.ID_CONVERTER.value}:device_id>'.format(DeviceDef),
view_func=device_view,
methods={'PUT'})
@option('-o', '--org', help=ORG_H) @option('-o', '--org', help=ORG_H)
@option('-p', '--provider', help=PROV_H) @option('-p', '--provider', help=PROV_H)

View File

@ -8,7 +8,6 @@ from ereuse_devicehub.resources.tag import Tag
class TagView(View): class TagView(View):
def post(self): def post(self):
"""Creates a tag.""" """Creates a tag."""
t = request.get_json() t = request.get_json()
@ -20,6 +19,31 @@ class TagView(View):
return Response(status=201) return Response(status=201)
class TagDeviceView(View):
"""Endpoints to work with the device of the tag; /tags/23/device"""
def one(self, id):
"""Gets the device from the tag."""
tag = Tag.from_an_id(id).one() # type: Tag
if not tag.device:
raise TagNotLinked(tag.id)
return app.resources[Device.t].schema.jsonify(tag.device)
# noinspection PyMethodOverriding
def put(self, tag_id: str, device_id: str):
"""Links an existing tag with a device."""
tag = Tag.from_an_id(tag_id).one() # type: Tag
if tag.device_id:
if tag.device_id == device_id:
return Response(status=204)
else:
raise LinkedToAnotherDevice(tag.device_id)
else:
tag.device_id = device_id
db.session.commit()
return Response(status=204)
def get_device_from_tag(id: str): def get_device_from_tag(id: str):
""" """
Gets the device by passing a tag id. Gets the device by passing a tag id.
@ -46,3 +70,9 @@ class CannotCreateETag(ValidationError):
def __init__(self, id: str): def __init__(self, id: str):
message = 'Only sysadmin can create an eReuse.org Tag ({})'.format(id) message = 'Only sysadmin can create an eReuse.org Tag ({})'.format(id)
super().__init__(message) super().__init__(message)
class LinkedToAnotherDevice(ValidationError):
def __init__(self, device_id: int):
message = 'The tag is already linked to device {}'.format(device_id)
super().__init__(message)

View File

@ -34,7 +34,7 @@ setup(
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
install_requires=[ install_requires=[
'teal>=0.2.0a15', # teal always first 'teal>=0.2.0a16', # teal always first
'click', 'click',
'click-spinner', 'click-spinner',
'ereuse-rate==0.0.2', 'ereuse-rate==0.0.2',

View File

@ -16,7 +16,7 @@ def test_api_docs(client: Client):
"""Tests /apidocs correct initialization.""" """Tests /apidocs correct initialization."""
docs, _ = client.get('/apidocs') docs, _ = client.get('/apidocs')
assert set(docs['paths'].keys()) == { assert set(docs['paths'].keys()) == {
'/tags/{id}/device', # todo this does not appear: '/tags/{id}/device',
'/inventories/', '/inventories/',
'/apidocs', '/apidocs',
'/users/', '/users/',
@ -27,7 +27,8 @@ def test_api_docs(client: Client):
'/events/', '/events/',
'/lots/', '/lots/',
'/lots/{id}/children', '/lots/{id}/children',
'/lots/{id}/devices' '/lots/{id}/devices',
'/tags/{tag_id}/device/{device_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'] == {

View File

@ -9,10 +9,11 @@ from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.agent.models import Organization from ereuse_devicehub.resources.agent.models import Organization
from ereuse_devicehub.resources.device.models import Desktop from ereuse_devicehub.resources.device.models import Desktop, Device
from ereuse_devicehub.resources.enums import ComputerChassis from ereuse_devicehub.resources.enums import ComputerChassis
from ereuse_devicehub.resources.tag import Tag from ereuse_devicehub.resources.tag import Tag
from ereuse_devicehub.resources.tag.view import CannotCreateETag, TagNotLinked from ereuse_devicehub.resources.tag.view import CannotCreateETag, LinkedToAnotherDevice, \
TagNotLinked
from tests import conftest from tests import conftest
@ -135,6 +136,26 @@ def test_tag_create_tags_cli(app: Devicehub, user: UserClient):
assert tag.org.id == Organization.get_default_org_id() assert tag.org.id == Organization.get_default_org_id()
def test_tag_manual_link(app: Devicehub, user: UserClient):
"""Tests linking manually a tag through PUT /tags/<id>/device/<id>"""
with app.app_context():
db.session.add(Tag('foo-bar', secondary='foo-sec'))
desktop = Desktop(serial_number='foo', chassis=ComputerChassis.AllInOne)
db.session.add(desktop)
db.session.commit()
desktop_id = desktop.id
user.put({}, res=Tag, item='foo-bar/device/{}'.format(desktop_id), status=204)
device, _ = user.get(res=Device, item=1)
assert device['tags'][0]['id'] == 'foo-bar'
# Device already linked
# Just returns an OK to conform to PUT as anything changes
user.put({}, res=Tag, item='foo-sec/device/{}'.format(desktop_id), status=204)
# cannot link to another device when already linked
user.put({}, res=Tag, item='foo-bar/device/99', status=LinkedToAnotherDevice)
@pytest.mark.usefixtures(conftest.app_context.__name__) @pytest.mark.usefixtures(conftest.app_context.__name__)
def test_tag_secondary(): def test_tag_secondary():
"""Creates and consumes tags with a secondary id.""" """Creates and consumes tags with a secondary id."""