Add things_response; create tags through ereuse Tag
This commit is contained in:
parent
9ec9fb0964
commit
74860be347
|
@ -111,4 +111,5 @@ class Devicehub(Teal):
|
||||||
def _prepare_request(self):
|
def _prepare_request(self):
|
||||||
"""Prepares request stuff."""
|
"""Prepares request stuff."""
|
||||||
inv = g.inventory = Inventory.current # type: Inventory
|
inv = g.inventory = Inventory.current # type: Inventory
|
||||||
g.tag_provider = DevicehubClient(base_url=inv.tag_provider, token=inv.tag_token)
|
g.tag_provider = DevicehubClient(base_url=inv.tag_provider,
|
||||||
|
token=DevicehubClient.encode_token(inv.tag_token))
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from flask import Response, jsonify, request
|
||||||
from teal.query import NestedQueryFlaskParser
|
from teal.query import NestedQueryFlaskParser
|
||||||
from webargs.flaskparser import FlaskParser
|
from webargs.flaskparser import FlaskParser
|
||||||
|
|
||||||
|
@ -10,3 +13,31 @@ class SearchQueryParser(NestedQueryFlaskParser):
|
||||||
else:
|
else:
|
||||||
v = super().parse_querystring(req, name, field)
|
v = super().parse_querystring(req, name, field)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def things_response(items: List[Dict],
|
||||||
|
page: int = None,
|
||||||
|
per_page: int = None,
|
||||||
|
total: int = None,
|
||||||
|
previous: int = None,
|
||||||
|
next: int = None,
|
||||||
|
url: str = None,
|
||||||
|
code: int = 200) -> Response:
|
||||||
|
"""Generates a Devicehub API list conformant response for multiple
|
||||||
|
things.
|
||||||
|
"""
|
||||||
|
response = jsonify({
|
||||||
|
'items': items,
|
||||||
|
# todo pagination should be in Header like github
|
||||||
|
# https://developer.github.com/v3/guides/traversing-with-pagination/
|
||||||
|
'pagination': {
|
||||||
|
'page': page,
|
||||||
|
'perPage': per_page,
|
||||||
|
'total': total,
|
||||||
|
'previous': previous,
|
||||||
|
'next': next
|
||||||
|
},
|
||||||
|
'url': url or request.path
|
||||||
|
})
|
||||||
|
response.status_code = code
|
||||||
|
return response
|
||||||
|
|
|
@ -11,7 +11,7 @@ from teal.resource import View
|
||||||
|
|
||||||
from ereuse_devicehub import auth
|
from ereuse_devicehub import auth
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.query import SearchQueryParser
|
from ereuse_devicehub.query import SearchQueryParser, things_response
|
||||||
from ereuse_devicehub.resources import search
|
from ereuse_devicehub.resources import search
|
||||||
from ereuse_devicehub.resources.device.models import Device, Manufacturer
|
from ereuse_devicehub.resources.device.models import Device, Manufacturer
|
||||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||||
|
@ -112,20 +112,10 @@ class DeviceView(View):
|
||||||
# Compute query
|
# Compute query
|
||||||
query = self.query(args)
|
query = self.query(args)
|
||||||
devices = query.paginate(page=args['page'], per_page=30) # type: Pagination
|
devices = query.paginate(page=args['page'], per_page=30) # type: Pagination
|
||||||
ret = {
|
return things_response(
|
||||||
'items': self.schema.dump(devices.items, many=True, nested=1),
|
self.schema.dump(devices.items, many=True, nested=1),
|
||||||
# todo pagination should be in Header like github
|
devices.page, devices.per_page, devices.total, devices.prev_num, devices.next_num
|
||||||
# https://developer.github.com/v3/guides/traversing-with-pagination/
|
)
|
||||||
'pagination': {
|
|
||||||
'page': devices.page,
|
|
||||||
'perPage': devices.per_page,
|
|
||||||
'total': devices.total,
|
|
||||||
'previous': devices.prev_num,
|
|
||||||
'next': devices.next_num
|
|
||||||
},
|
|
||||||
'url': request.path
|
|
||||||
}
|
|
||||||
return jsonify(ret)
|
|
||||||
|
|
||||||
def query(self, args):
|
def query(self, args):
|
||||||
query = Device.query.distinct() # todo we should not force to do this if the query is ok
|
query = Device.query.distinct() # todo we should not force to do this if the query is ok
|
||||||
|
|
|
@ -10,6 +10,7 @@ from teal.marshmallow import EnumField
|
||||||
from teal.resource import View
|
from teal.resource import View
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.query import things_response
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
from ereuse_devicehub.resources.lot.models import Lot, Path
|
from ereuse_devicehub.resources.lot.models import Lot, Path
|
||||||
|
|
||||||
|
@ -78,17 +79,10 @@ class LotView(View):
|
||||||
if args['search']:
|
if args['search']:
|
||||||
query = query.filter(Lot.name.ilike(args['search'] + '%'))
|
query = query.filter(Lot.name.ilike(args['search'] + '%'))
|
||||||
lots = query.paginate(per_page=6 if args['search'] else 30)
|
lots = query.paginate(per_page=6 if args['search'] else 30)
|
||||||
ret = {
|
return things_response(
|
||||||
'items': self.schema.dump(lots.items, many=True, nested=0),
|
self.schema.dump(lots.items, many=True, nested=0),
|
||||||
'pagination': {
|
lots.page, lots.per_page, lots.total, lots.prev_num, lots.next_num
|
||||||
'page': lots.page,
|
)
|
||||||
'perPage': lots.per_page,
|
|
||||||
'total': lots.total,
|
|
||||||
'previous': lots.prev_num,
|
|
||||||
'next': lots.next_num
|
|
||||||
},
|
|
||||||
'url': request.path
|
|
||||||
}
|
|
||||||
return jsonify(ret)
|
return jsonify(ret)
|
||||||
|
|
||||||
def delete(self, id):
|
def delete(self, id):
|
||||||
|
|
|
@ -9,6 +9,7 @@ from teal.db import DB_CASCADE_SET_NULL, Query, URL, check_lower
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from teal.resource import url_for_resource
|
from teal.resource import url_for_resource
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.agent.models import Organization
|
from ereuse_devicehub.resources.agent.models import Organization
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
@ -23,7 +24,7 @@ class Tags(Set['Tag']):
|
||||||
|
|
||||||
|
|
||||||
class Tag(Thing):
|
class Tag(Thing):
|
||||||
id = Column(Unicode(), check_lower('id'), primary_key=True)
|
id = Column(db.CIText(), primary_key=True)
|
||||||
id.comment = """The ID of the tag."""
|
id.comment = """The ID of the tag."""
|
||||||
org_id = Column(UUID(as_uuid=True),
|
org_id = Column(UUID(as_uuid=True),
|
||||||
ForeignKey(Organization.id),
|
ForeignKey(Organization.id),
|
||||||
|
@ -49,7 +50,7 @@ class Tag(Thing):
|
||||||
backref=backref('tags', lazy=True, collection_class=Tags),
|
backref=backref('tags', lazy=True, collection_class=Tags),
|
||||||
primaryjoin=Device.id == device_id)
|
primaryjoin=Device.id == device_id)
|
||||||
"""The device linked to this tag."""
|
"""The device linked to this tag."""
|
||||||
secondary = Column(Unicode(), check_lower('secondary'), index=True)
|
secondary = Column(db.CIText(), index=True)
|
||||||
secondary.comment = """
|
secondary.comment = """
|
||||||
A secondary identifier for this tag. It has the same
|
A secondary identifier for this tag. It has the same
|
||||||
constraints as the main one. Only needed in special cases.
|
constraints as the main one. Only needed in special cases.
|
||||||
|
@ -108,7 +109,12 @@ class Tag(Thing):
|
||||||
Only tags that are from the default organization can be
|
Only tags that are from the default organization can be
|
||||||
printed by the user.
|
printed by the user.
|
||||||
"""
|
"""
|
||||||
return Organization.get_default_org_id() == self.org_id
|
return self.org_id == Organization.get_default_org_id()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_printable_q(cls):
|
||||||
|
"""Return a SQLAlchemy filter expression for printable queries"""
|
||||||
|
return cls.org_id == Organization.get_default_org_id()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<Tag {0.id} org:{0.org_id} device:{0.device_id}>'.format(self)
|
return '<Tag {0.id} org:{0.org_id} device:{0.device_id}>'.format(self)
|
||||||
|
|
|
@ -45,6 +45,10 @@ class Tag(Thing):
|
||||||
def printable(self) -> bool:
|
def printable(self) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_printable_q(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> urlutils.URL:
|
def url(self) -> urlutils.URL:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from flask import Response, current_app as app, g, jsonify, redirect, request
|
from flask import Response, current_app as app, g, redirect, request
|
||||||
|
from flask_sqlalchemy import Pagination
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
from teal.resource import View, url_for_resource
|
from teal.resource import View, url_for_resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.query import things_response
|
||||||
from ereuse_devicehub.resources.device.models import Device
|
from ereuse_devicehub.resources.device.models import Device
|
||||||
from ereuse_devicehub.resources.tag import Tag
|
from ereuse_devicehub.resources.tag import Tag
|
||||||
|
|
||||||
|
@ -17,14 +19,21 @@ class TagView(View):
|
||||||
res = self._post_one()
|
res = self._post_one()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def find(self, args: dict):
|
||||||
|
tags = Tag.query.filter(Tag.is_printable_q()) \
|
||||||
|
.order_by(Tag.created.desc()) \
|
||||||
|
.paginate(per_page=200) # type: Pagination
|
||||||
|
return things_response(
|
||||||
|
self.schema.dump(tags.items, many=True, nested=0),
|
||||||
|
tags.page, tags.per_page, tags.total, tags.prev_num, tags.next_num
|
||||||
|
)
|
||||||
|
|
||||||
def _create_many_regular_tags(self, num: int):
|
def _create_many_regular_tags(self, num: int):
|
||||||
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
|
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
|
||||||
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
|
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
|
||||||
db.session.add_all(tags)
|
db.session.add_all(tags)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
response = jsonify(items=self.schema.dump(tags, many=True, nested=1)) # type: Response
|
return things_response(self.schema.dump(tags, many=True, nested=1), code=201)
|
||||||
response.status_code = 201
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _post_one(self):
|
def _post_one(self):
|
||||||
# todo do we use this?
|
# todo do we use this?
|
||||||
|
|
|
@ -5,7 +5,7 @@ click==6.7
|
||||||
click-spinner==0.1.8
|
click-spinner==0.1.8
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
colour==0.1.5
|
colour==0.1.5
|
||||||
ereuse-utils[naming, test, session, cli]==0.4.0b14
|
ereuse-utils[naming, test, session, cli]==0.4.0b15
|
||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
Flask-Cors==3.0.6
|
Flask-Cors==3.0.6
|
||||||
Flask-SQLAlchemy==2.3.2
|
Flask-SQLAlchemy==2.3.2
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -32,7 +32,7 @@ setup(
|
||||||
'teal>=0.2.0a35', # teal always first
|
'teal>=0.2.0a35', # teal always first
|
||||||
'click',
|
'click',
|
||||||
'click-spinner',
|
'click-spinner',
|
||||||
'ereuse-utils[naming, test, session, cli]>=0.4b14',
|
'ereuse-utils[naming, test, session, cli]>=0.4b15',
|
||||||
'hashids',
|
'hashids',
|
||||||
'marshmallow_enum',
|
'marshmallow_enum',
|
||||||
'psycopg2-binary',
|
'psycopg2-binary',
|
||||||
|
|
|
@ -3,6 +3,7 @@ import pathlib
|
||||||
import pytest
|
import pytest
|
||||||
import requests_mock
|
import requests_mock
|
||||||
from boltons.urlutils import URL
|
from boltons.urlutils import URL
|
||||||
|
from ereuse_utils.session import DevicehubClient
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
from teal.db import MultipleResourcesFound, ResourceNotFound, UniqueViolation
|
from teal.db import MultipleResourcesFound, ResourceNotFound, UniqueViolation
|
||||||
from teal.marshmallow import ValidationError
|
from teal.marshmallow import ValidationError
|
||||||
|
@ -242,7 +243,8 @@ def test_crate_num_regular_tags(user: UserClient, requests_mock: requests_mock.m
|
||||||
requests_mock.post('https://example.com/',
|
requests_mock.post('https://example.com/',
|
||||||
# request
|
# request
|
||||||
request_headers={
|
request_headers={
|
||||||
'Authorization': 'Basic 52dacef0-6bcb-4919-bfed-f10d2c96ecee'
|
'Authorization': 'Basic {}'.format(DevicehubClient.encode_token(
|
||||||
|
'52dacef0-6bcb-4919-bfed-f10d2c96ecee'))
|
||||||
},
|
},
|
||||||
# response
|
# response
|
||||||
json=['tag1id', 'tag2id'],
|
json=['tag1id', 'tag2id'],
|
||||||
|
@ -252,3 +254,37 @@ def test_crate_num_regular_tags(user: UserClient, requests_mock: requests_mock.m
|
||||||
assert data['items'][0]['printable'], 'Tags made this way are printable'
|
assert data['items'][0]['printable'], 'Tags made this way are printable'
|
||||||
assert data['items'][1]['id'] == 'tag2id'
|
assert data['items'][1]['id'] == 'tag2id'
|
||||||
assert data['items'][1]['printable']
|
assert data['items'][1]['printable']
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_tags_endpoint(user: UserClient, app: Devicehub,
|
||||||
|
requests_mock: requests_mock.mocker.Mocker):
|
||||||
|
"""Performs GET /tags after creating 3 tags, 2 printable and one
|
||||||
|
not. Only the printable ones are returned.
|
||||||
|
"""
|
||||||
|
# Prepare test
|
||||||
|
with app.app_context():
|
||||||
|
org = Organization(name='bar', tax_id='bartax')
|
||||||
|
tag = Tag(id='bar-1', org=org, provider=URL('http://foo.bar'))
|
||||||
|
db.session.add(tag)
|
||||||
|
db.session.commit()
|
||||||
|
assert not tag.printable
|
||||||
|
|
||||||
|
requests_mock.post('https://example.com/',
|
||||||
|
# request
|
||||||
|
request_headers={
|
||||||
|
'Authorization': 'Basic {}'.format(DevicehubClient.encode_token(
|
||||||
|
'52dacef0-6bcb-4919-bfed-f10d2c96ecee'))
|
||||||
|
},
|
||||||
|
# response
|
||||||
|
json=['tag1id', 'tag2id'],
|
||||||
|
status_code=201)
|
||||||
|
user.post({}, res=Tag, query=[('num', 2)])
|
||||||
|
|
||||||
|
# Test itself
|
||||||
|
data, _ = user.get(res=Tag)
|
||||||
|
assert len(data['items']) == 2, 'Only 2 tags are printable, thus retreived'
|
||||||
|
# Order is created descending
|
||||||
|
assert data['items'][0]['id'] == 'tag2id'
|
||||||
|
assert data['items'][0]['printable']
|
||||||
|
assert data['items'][1]['id'] == 'tag1id'
|
||||||
|
assert data['items'][1]['printable'], 'Tags made this way are printable'
|
||||||
|
|
Reference in a new issue