diff --git a/ereuse_devicehub/devicehub.py b/ereuse_devicehub/devicehub.py index 17061e64..99dca4e0 100644 --- a/ereuse_devicehub/devicehub.py +++ b/ereuse_devicehub/devicehub.py @@ -111,4 +111,5 @@ class Devicehub(Teal): def _prepare_request(self): """Prepares request stuff.""" 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)) diff --git a/ereuse_devicehub/query.py b/ereuse_devicehub/query.py index 463fc0b2..c5bd1528 100644 --- a/ereuse_devicehub/query.py +++ b/ereuse_devicehub/query.py @@ -1,3 +1,6 @@ +from typing import Dict, List + +from flask import Response, jsonify, request from teal.query import NestedQueryFlaskParser from webargs.flaskparser import FlaskParser @@ -10,3 +13,31 @@ class SearchQueryParser(NestedQueryFlaskParser): else: v = super().parse_querystring(req, name, field) 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 diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index 1ec1736a..8dbcec33 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -11,7 +11,7 @@ from teal.resource import View from ereuse_devicehub import auth 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.device.models import Device, Manufacturer from ereuse_devicehub.resources.device.search import DeviceSearch @@ -112,20 +112,10 @@ class DeviceView(View): # Compute query query = self.query(args) devices = query.paginate(page=args['page'], per_page=30) # type: Pagination - ret = { - 'items': self.schema.dump(devices.items, many=True, nested=1), - # todo pagination should be in Header like github - # 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) + return things_response( + self.schema.dump(devices.items, many=True, nested=1), + devices.page, devices.per_page, devices.total, devices.prev_num, devices.next_num + ) def query(self, args): query = Device.query.distinct() # todo we should not force to do this if the query is ok diff --git a/ereuse_devicehub/resources/lot/views.py b/ereuse_devicehub/resources/lot/views.py index 32aaa179..fb502da6 100644 --- a/ereuse_devicehub/resources/lot/views.py +++ b/ereuse_devicehub/resources/lot/views.py @@ -10,6 +10,7 @@ from teal.marshmallow import EnumField from teal.resource import View 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.lot.models import Lot, Path @@ -78,17 +79,10 @@ class LotView(View): if args['search']: query = query.filter(Lot.name.ilike(args['search'] + '%')) lots = query.paginate(per_page=6 if args['search'] else 30) - ret = { - 'items': self.schema.dump(lots.items, many=True, nested=0), - 'pagination': { - 'page': lots.page, - 'perPage': lots.per_page, - 'total': lots.total, - 'previous': lots.prev_num, - 'next': lots.next_num - }, - 'url': request.path - } + return things_response( + self.schema.dump(lots.items, many=True, nested=0), + lots.page, lots.per_page, lots.total, lots.prev_num, lots.next_num + ) return jsonify(ret) def delete(self, id): diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 850e09e6..49e2a2f4 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -9,6 +9,7 @@ from teal.db import DB_CASCADE_SET_NULL, Query, URL, check_lower from teal.marshmallow import ValidationError 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.device.models import Device from ereuse_devicehub.resources.models import Thing @@ -23,7 +24,7 @@ class Tags(Set['Tag']): 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.""" org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), @@ -49,7 +50,7 @@ class Tag(Thing): backref=backref('tags', lazy=True, collection_class=Tags), primaryjoin=Device.id == device_id) """The device linked to this tag.""" - secondary = Column(Unicode(), check_lower('secondary'), index=True) + secondary = Column(db.CIText(), index=True) secondary.comment = """ A secondary identifier for this tag. It has the same 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 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: return ''.format(self) diff --git a/ereuse_devicehub/resources/tag/model.pyi b/ereuse_devicehub/resources/tag/model.pyi index 8e365551..37c47ddf 100644 --- a/ereuse_devicehub/resources/tag/model.pyi +++ b/ereuse_devicehub/resources/tag/model.pyi @@ -45,6 +45,10 @@ class Tag(Thing): def printable(self) -> bool: pass + @classmethod + def is_printable_q(cls): + pass + @property def url(self) -> urlutils.URL: pass diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 7140573b..f337c8de 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -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.resource import View, url_for_resource 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.tag import Tag @@ -17,14 +19,21 @@ class TagView(View): res = self._post_one() 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): 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] db.session.add_all(tags) db.session.commit() - response = jsonify(items=self.schema.dump(tags, many=True, nested=1)) # type: Response - response.status_code = 201 - return response + return things_response(self.schema.dump(tags, many=True, nested=1), code=201) def _post_one(self): # todo do we use this? diff --git a/requirements.txt b/requirements.txt index 631e7e39..0bb9cb3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ click==6.7 click-spinner==0.1.8 colorama==0.3.9 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-Cors==3.0.6 Flask-SQLAlchemy==2.3.2 diff --git a/setup.py b/setup.py index 1cce365e..1b17d7a9 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup( 'teal>=0.2.0a35', # teal always first 'click', 'click-spinner', - 'ereuse-utils[naming, test, session, cli]>=0.4b14', + 'ereuse-utils[naming, test, session, cli]>=0.4b15', 'hashids', 'marshmallow_enum', 'psycopg2-binary', diff --git a/tests/test_tag.py b/tests/test_tag.py index 5353db25..47bd2213 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -3,6 +3,7 @@ import pathlib import pytest import requests_mock from boltons.urlutils import URL +from ereuse_utils.session import DevicehubClient from pytest import raises from teal.db import MultipleResourcesFound, ResourceNotFound, UniqueViolation 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/', # request request_headers={ - 'Authorization': 'Basic 52dacef0-6bcb-4919-bfed-f10d2c96ecee' + 'Authorization': 'Basic {}'.format(DevicehubClient.encode_token( + '52dacef0-6bcb-4919-bfed-f10d2c96ecee')) }, # response 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'][1]['id'] == 'tag2id' 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'