Add secondary id to tags

This commit is contained in:
Xavier Bustamante Talavera 2018-09-20 09:28:52 +02:00
parent 543e457581
commit 3b0f483a90
15 changed files with 219 additions and 101 deletions

View File

@ -19,11 +19,9 @@ A device can have many tags but a tag can only be linked to one device.
As for the actual implementation, you cannot unlink them. As for the actual implementation, you cannot unlink them.
Devicehub users can design, generate and print tags, manually setting Devicehub users can design, generate and print tags, manually setting
an ID and an tag provider. Future Devicehub versions can allow an ID and a tag provider. Note though that these virtual tags don't have
parametrizing an ID generator. to forcefully be printed or have a physical representation
(this is not imposed at system level).
Note that these virtual tags don't have to forcefully be printed or
have a physical representation (this is not imposed at system level).
eTags eTags
***** *****
@ -33,12 +31,16 @@ by tag providers that comply with the eReuse.org requisites.
The eTags are designed to empower device exchange between The eTags are designed to empower device exchange between
organizations and identification efficiency. They are built with durable organizations and identification efficiency. They are built with durable
plastic and have a QR code, NFC chip and a written ID. plastic and have a QR code, a NFC chip and a written ID.
These tags live in separate databases from Devicehubs, empowered by These tags live in separate databases from Devicehubs, empowered by
the `eReuse.org Tag <https://github.com/ereuse/tag>`_. By using this the `eReuse.org Tag <https://github.com/ereuse/tag>`_ software.
software, eReuse.org certified tag providers can create and manage By using this software, eReuse.org certified tag providers
the tags, and send them to Devicehubs of their choice. can create and manage the tags, and send them to Devicehubs of their
choice.
The section *Use-case with eTags* shows the use-case of these
eTags.
Tag ID design Tag ID design
============= =============
@ -95,16 +97,13 @@ Getting a device through its tag
When performing ``GET /tags/<tag-id>/device`` you will get directly the When performing ``GET /tags/<tag-id>/device`` you will get directly the
device of such tag, as long as there are not two tags with the same device of such tag, as long as there are not two tags with the same
tag-id. In such case you should use ``GET /tags/<ngo>/<tag-id>/device`` tag-id. In such case you should use ``GET /tags/<ngo>/<tag-id>/device``
to inequivocally get the correct device (to develop). to unequivocally get the correct device (feature to develop).
Tags and migrations Tags and migrations
******************* *******************
Tags travel with the devices they are linked when migrating them. Future Tags travel with the devices they are linked when migrating them. Future
implementations can parameterize this. implementations can parameterize this.
http://t.devicetag.io/TG-1234567890
Use-case with eTags Use-case with eTags
******************* *******************
We explain the use-case of tagging a device with an :ref:`tags:eTags`, We explain the use-case of tagging a device with an :ref:`tags:eTags`,

View File

@ -37,7 +37,8 @@ class Dummy:
with click_spinner.spinner(): with click_spinner.spinner():
self.app.init_db(erase=True) self.app.init_db(erase=True)
user = self.user_client('user@dhub.com', '1234') user = self.user_client('user@dhub.com', '1234')
user.post(res=Tag, query=[('ids', i) for i in self.TAGS], data={}) for id in self.TAGS:
user.post({'id': id}, res=Tag)
files = tuple(Path(__file__).parent.joinpath('files').iterdir()) files = tuple(Path(__file__).parent.joinpath('files').iterdir())
print('done.') print('done.')
with click.progressbar(files, label='Creating devices...'.ljust(28)) as bar: with click.progressbar(files, label='Creating devices...'.ljust(28)) as bar:

View File

@ -6,11 +6,12 @@ from flask import current_app as app, g
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import backref, relationship from sqlalchemy.orm import backref, relationship, validates
from sqlalchemy_utils import EmailType, PhoneNumberType from sqlalchemy_utils import EmailType, PhoneNumberType
from teal import enums from teal import enums
from teal.db import INHERIT_COND, POLYMORPHIC_ID, \ from teal.db import INHERIT_COND, POLYMORPHIC_ID, \
POLYMORPHIC_ON POLYMORPHIC_ON
from teal.marshmallow import ValidationError
from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE, Thing from ereuse_devicehub.resources.models import STR_SIZE, STR_SM_SIZE, Thing
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
@ -72,6 +73,12 @@ class Agent(Thing):
# todo test # todo test
return sorted(chain(self.events_agent, self.events_to), key=attrgetter('created')) return sorted(chain(self.events_agent, self.events_to), key=attrgetter('created'))
@validates('name')
def does_not_contain_slash(self, _, value: str):
if '/' in value:
raise ValidationError('Name cannot contain slash \'')
return value
def __repr__(self) -> str: def __repr__(self) -> str:
return '<{0.t} {0.name}>'.format(self) return '<{0.t} {0.name}>'.format(self)

View File

@ -155,7 +155,7 @@ class Sync:
with suppress(ResourceNotFound): with suppress(ResourceNotFound):
db_device = Device.query.filter_by(hid=device.hid).one() db_device = Device.query.filter_by(hid=device.hid).one()
try: try:
tags = {Tag.query.filter_by(id=tag.id).one() for tag in device.tags} # type: Set[Tag] tags = {Tag.from_an_id(tag.id).one() for tag in device.tags} # type: Set[Tag]
except ResourceNotFound: except ResourceNotFound:
raise ResourceNotFound('tag you are linking to device {}'.format(device)) raise ResourceNotFound('tag you are linking to device {}'.format(device))
linked_tags = {tag for tag in tags if tag.device_id} # type: Set[Tag] linked_tags = {tag for tag in tags if tag.device_id} # type: Set[Tag]

View File

@ -89,7 +89,8 @@ class LotDeviceView(LotBaseChildrenView):
""" """
def _post(self, lot: Lot, ids: Set[uuid.UUID]): def _post(self, lot: Lot, ids: Set[uuid.UUID]):
lot.devices |= self.get_ids() # todo this works?
lot.devices |= ids
def _delete(self, lot: Lot, ids: Set[uuid.UUID]): def _delete(self, lot: Lot, ids: Set[uuid.UUID]):
lot.devices -= self.get_ids() lot.devices -= ids

View File

@ -1,6 +1,8 @@
from typing import Tuple import csv
import pathlib
from click import argument, option from click import argument, option
from ereuse_utils import cli
from teal.resource import Resource from teal.resource import Resource
from teal.teal import Teal from teal.teal import Teal
@ -14,12 +16,19 @@ class TagDef(Resource):
SCHEMA = schema.Tag SCHEMA = schema.Tag
VIEW = TagView VIEW = TagView
ORG_H = 'The name of an existing organization in the DB. '
'By default the organization operating this Devicehub.'
PROV_H = 'The Base URL of the provider. '
'By default set to the actual Devicehub.'
CLI_SCHEMA = schema.Tag(only=('id', 'provider', 'org', 'secondary'))
def __init__(self, app: Teal, import_name=__package__, static_folder=None, def __init__(self, app: Teal, import_name=__package__, static_folder=None,
static_url_path=None, static_url_path=None,
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
root_path=None): root_path=None):
cli_commands = ( cli_commands = (
(self.create_tags, 'create-tags'), (self.create_tag, 'create-tag'),
(self.create_tags_csv, 'create-tags-csv')
) )
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)
@ -27,20 +36,37 @@ class TagDef(Resource):
self.add_url_rule('/<{}:{}>/device'.format(self.ID_CONVERTER.value, self.ID_NAME), self.add_url_rule('/<{}:{}>/device'.format(self.ID_CONVERTER.value, self.ID_NAME),
view_func=_get_device_from_tag, view_func=_get_device_from_tag,
methods={'GET'}) methods={'GET'})
self.tag_schema = schema.Tag
@option('--org', @option('-o', '--org', help=ORG_H)
help='The name of an existing organization in the DB. ' @option('-p', '--provider', help=PROV_H)
'By default the organization operating this Devicehub.') @option('-s', '--sec', help=Tag.secondary.comment)
@option('--provider', @argument('id')
help='The Base URL of the provider. ' def create_tag(self,
'By default set to the actual Devicehub.') id: str,
@argument('ids', nargs=-1, required=True) org: str = None,
def create_tags(self, ids: Tuple[str], org: str = None, provider: str = None): sec: str = None,
provider: str = None):
"""Create TAGS and associates them to a specific PROVIDER.""" """Create TAGS and associates them to a specific PROVIDER."""
tag_schema = schema.Tag(only=('id', 'provider', 'org')) db.session.add(Tag(**self.schema.load(
dict(id=id, org=org, secondary=sec, provider=provider)
db.session.add_all( )))
Tag(**tag_schema.load({'id': tag_id, 'provider': provider, 'org': org})) db.session.commit()
for tag_id in ids
) @option('--org', help=ORG_H)
@option('--provider', help=PROV_H)
@argument('path', type=cli.Path(writable=True))
def create_tags_csv(self, path: pathlib.Path, org: str, provider: str):
"""Creates tags by reading CSV from ereuse-tag.
CSV must have the following columns:
1. ID tag
2. Secondary id tag (or empty)
"""
with path.open() as f:
for id, sec in csv.reader(f):
db.session.add(Tag(**self.schema.load(
dict(id=id, org=org, secondary=sec, provider=provider)
)))
db.session.commit() db.session.commit()

View File

@ -1,7 +1,9 @@
from contextlib import suppress
from sqlalchemy import BigInteger, Column, ForeignKey, Unicode, UniqueConstraint from sqlalchemy import BigInteger, Column, ForeignKey, Unicode, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import backref, relationship, validates from sqlalchemy.orm import backref, relationship, validates
from teal.db import DB_CASCADE_SET_NULL, URL from teal.db import DB_CASCADE_SET_NULL, Query, URL
from teal.marshmallow import ValidationError from teal.marshmallow import ValidationError
from ereuse_devicehub.resources.agent.models import Organization from ereuse_devicehub.resources.agent.models import Organization
@ -11,6 +13,7 @@ from ereuse_devicehub.resources.models import Thing
class Tag(Thing): class Tag(Thing):
id = Column(Unicode(), primary_key=True) id = Column(Unicode(), primary_key=True)
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),
primary_key=True, primary_key=True,
@ -22,23 +25,52 @@ class Tag(Thing):
backref=backref('tags', lazy=True), backref=backref('tags', lazy=True),
primaryjoin=Organization.id == org_id, primaryjoin=Organization.id == org_id,
collection_class=set) collection_class=set)
"""The organization that issued the tag."""
provider = Column(URL(), provider = Column(URL(),
comment='The tag provider URL. If None, the provider is this Devicehub.') comment='The tag provider URL. If None, the provider is this Devicehub.')
provider.comment = """The provider URL."""
device_id = Column(BigInteger, device_id = Column(BigInteger,
# We don't want to delete the tag on device deletion, only set to null # We don't want to delete the tag on device deletion, only set to null
ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL)) ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL))
device = relationship(Device, device = relationship(Device,
backref=backref('tags', lazy=True, collection_class=set), backref=backref('tags', lazy=True, collection_class=set),
primaryjoin=Device.id == device_id) primaryjoin=Device.id == device_id)
"""The device linked to this tag."""
secondary = Column(Unicode())
secondary.comment = """
A secondary identifier for this tag. It has the same
constraints as the main one.
@validates('id') Only needed in special cases.
"""
def __init__(self, id: str, **kwargs) -> None:
super().__init__(id=id, **kwargs)
def like_etag(self):
"""Checks if the tag conforms to the `eTag spec <http:
//devicehub.ereuse.org/tags.html#etags>`_.
"""
with suppress(ValueError):
provider, id = self.id.split('-')
if len(provider) == 2 and 5 <= len(id) <= 10:
return True
return False
@classmethod
def from_an_id(cls, id: str) -> Query:
"""Query to look for a tag from a possible identifier."""
return cls.query.filter((cls.id == id) | (cls.secondary == id))
@validates('id', 'secondary')
def does_not_contain_slash(self, _, value: str): def does_not_contain_slash(self, _, value: str):
if '/' in value: if '/' in value:
raise ValidationError('Tags cannot contain slashes (/).') raise ValidationError('Tags cannot contain slashes (/).')
return value return value
__table_args__ = ( __table_args__ = (
UniqueConstraint(device_id, org_id, name='one_tag_per_organization'), UniqueConstraint(device_id, org_id, name='one_tag_per_org'),
UniqueConstraint(secondary, org_id, name='one_secondary_per_org')
) )
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@ -3,7 +3,9 @@ from uuid import UUID
from boltons.urlutils import URL from boltons.urlutils import URL
from sqlalchemy import Column from sqlalchemy import Column
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from teal.db import Query
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
@ -15,11 +17,25 @@ class Tag(Thing):
provider = ... # type: Column provider = ... # type: Column
device_id = ... # type: Column device_id = ... # type: Column
device = ... # type: relationship device = ... # type: relationship
secondary = ... # type: Column
def __init__(self, **kwargs) -> None: def __init__(self, id: str,
super().__init__(**kwargs) org: Organization = None,
secondary: str = None,
provider: URL = None,
device: Device = None) -> None:
super().__init__()
self.id = ... # type: str self.id = ... # type: str
self.org_id = ... # type: UUID self.org_id = ... # type: UUID
self.org = ... # type: Organization
self.provider = ... # type: URL self.provider = ... # type: URL
self.device_id = ... # type: int self.device_id = ... # type: int
self.device = ... # type: Device self.device = ... # type: Device
self.secondary = ... # type: str
@classmethod
def from_an_id(cls, id: str) -> Query:
pass
def like_etag(self) -> bool:
pass

View File

@ -4,12 +4,20 @@ from teal.marshmallow import URL
from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.marshmallow import NestedOn
from ereuse_devicehub.resources.device.schemas import Device from ereuse_devicehub.resources.device.schemas import Device
from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.schemas import Thing
from ereuse_devicehub.resources.tag import model as m
def without_slash(x: str) -> bool:
"""Returns true if x does not contain a slash."""
return '/' not in x
class Tag(Thing): class Tag(Thing):
id = String(description='The ID of the tag.', id = String(description=m.Tag.id.comment,
validator=lambda x: '/' not in x, validator=without_slash,
required=True) required=True)
provider = URL(description='The provider URL.') provider = URL(description=m.Tag.provider.comment,
device = NestedOn(Device, description='The device linked to this tag.') validator=without_slash)
org = String(description='The organization that issued the tag.') device = NestedOn(Device, dump_only=True)
org = String()
secondary = String(description=m.Tag.secondary.comment)

View File

@ -1,44 +1,22 @@
from flask import Response, current_app as app, request from flask import Response, current_app as app, request
from marshmallow.fields import List, String, URL
from teal.marshmallow import ValidationError from teal.marshmallow import ValidationError
from teal.resource import Schema, View from teal.resource import View
from webargs.flaskparser import parser
from ereuse_devicehub.db import db
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
class TagView(View): class TagView(View):
class PostArgs(Schema):
ids = List(String(), required=True, description='A list of tags identifiers.')
org = String(description='The name of an existing organization in the DB. '
'If not set, the default organization is used.')
provider = URL(description='The Base URL of the provider. By default is this Devicehub.')
post_args = PostArgs()
def post(self): def post(self):
""" """Creates a tag."""
Creates tags. t = request.get_json()
tag = Tag(**t)
--- if tag.like_etag():
parameters: raise CannotCreateETag(tag.id)
- name: tags db.session.add(tag)
in: path db.session.commit()
description: Number of tags to create.
"""
args = parser.parse(self.post_args, request, locations={'querystring'})
# Ensure user is not POSTing an eReuse.org tag
# For now only allow them to be created through command-line
for id in args['ids']:
try:
provider, _id = id.split('-')
except ValueError:
pass
else:
if len(provider) == 2 and 5 <= len(_id) <= 10:
raise CannotCreateETag(id)
self.resource_def.create_tags(**args)
return Response(status=201) return Response(status=201)
@ -58,13 +36,13 @@ def get_device_from_tag(id: str):
return app.resources[Device.t].schema.jsonify(device) return app.resources[Device.t].schema.jsonify(device)
class CannotCreateETag(ValidationError):
def __init__(self, id: str):
message = 'Only sysadmin can create an eReuse.org Tag ({})'.format(id)
super().__init__(message)
class TagNotLinked(ValidationError): class TagNotLinked(ValidationError):
def __init__(self, id): def __init__(self, id):
message = 'The tag {} is not linked to a device.'.format(id) message = 'The tag {} is not linked to a device.'.format(id)
super().__init__(message, field_names=['device']) super().__init__(message, field_names=['device'])
class CannotCreateETag(ValidationError):
def __init__(self, id: str):
message = 'Only sysadmin can create an eReuse.org Tag ({})'.format(id)
super().__init__(message)

4
tests/files/tags-cli.csv Normal file
View File

@ -0,0 +1,4 @@
id1,sec1
id2,sec2
id3,sec3
id4,sec4
1 id1 sec1
2 id2 sec2
3 id3 sec3
4 id4 sec4

View File

@ -1,6 +1,7 @@
from uuid import UUID from uuid import UUID
import pytest import pytest
from marshmallow import ValidationError
from sqlalchemy_utils import PhoneNumber from sqlalchemy_utils import PhoneNumber
from teal.db import UniqueViolation from teal.db import UniqueViolation
from teal.enums import Country from teal.enums import Country
@ -127,3 +128,9 @@ def test_create_organization_main_method(app: Devicehub):
assert org.name == o['name'] == 'ACME' assert org.name == o['name'] == 'ACME'
assert org.tax_id == o['taxId'] == 'FOO' assert org.tax_id == o['taxId'] == 'FOO'
assert org.country.name == o['country'] == 'ES' assert org.country.name == o['country'] == 'ES'
@pytest.mark.usefixtures(app_context.__name__)
def test_organization_no_slash_name():
with pytest.raises(ValidationError):
Organization(name='/')

View File

@ -3,8 +3,10 @@ from uuid import UUID
import pytest import pytest
from colour import Color from colour import Color
from ereuse_utils.naming import Naming
from pytest import raises from pytest import raises
from sqlalchemy.util import OrderedSet from sqlalchemy.util import OrderedSet
from teal.db import ResourceNotFound
from ereuse_devicehub.client import UserClient from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
@ -20,8 +22,6 @@ from ereuse_devicehub.resources.enums import ComputerChassis, DisplayTech
from ereuse_devicehub.resources.event.models import Remove, Test from ereuse_devicehub.resources.event.models import Remove, Test
from ereuse_devicehub.resources.tag.model import Tag from ereuse_devicehub.resources.tag.model import Tag
from ereuse_devicehub.resources.user import User from ereuse_devicehub.resources.user import User
from ereuse_utils.naming import Naming
from teal.db import ResourceNotFound
from tests import conftest from tests import conftest
@ -217,7 +217,8 @@ def test_sync_execute_register_Desktop_existing_no_tag():
db.session.add(pc) db.session.add(pc)
db.session.commit() db.session.commit()
pc = Desktop(**conftest.file('pc-components.db')['device']) # Create a new transient non-db object pc = Desktop(
**conftest.file('pc-components.db')['device']) # Create a new transient non-db object
# 1: device exists on DB # 1: device exists on DB
db_pc = Sync().execute_register(pc) db_pc = Sync().execute_register(pc)
assert pc.physical_properties == db_pc.physical_properties assert pc.physical_properties == db_pc.physical_properties
@ -287,7 +288,7 @@ def test_sync_execute_register_tag_does_not_exist():
Tags have to be created before trying to link them through a Snapshot. Tags have to be created before trying to link them through a Snapshot.
""" """
pc = Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([Tag()])) pc = Desktop(**conftest.file('pc-components.db')['device'], tags=OrderedSet([Tag('foo')]))
with raises(ResourceNotFound): with raises(ResourceNotFound):
Sync().execute_register(pc) Sync().execute_register(pc)
@ -304,7 +305,8 @@ def test_sync_execute_register_tag_linked_same_device():
db.session.add(Tag(id='foo', device=orig_pc)) db.session.add(Tag(id='foo', device=orig_pc))
db.session.commit() db.session.commit()
pc = Desktop(**conftest.file('pc-components.db')['device']) # Create a new transient non-db object pc = Desktop(
**conftest.file('pc-components.db')['device']) # Create a new transient non-db object
pc.tags.add(Tag(id='foo')) pc.tags.add(Tag(id='foo'))
db_pc = Sync().execute_register(pc) db_pc = Sync().execute_register(pc)
assert db_pc.id == orig_pc.id assert db_pc.id == orig_pc.id
@ -326,7 +328,8 @@ def test_sync_execute_register_tag_linked_other_device_mismatch_between_tags():
db.session.add(Tag(id='foo-2', device=pc2)) db.session.add(Tag(id='foo-2', device=pc2))
db.session.commit() db.session.commit()
pc1 = Desktop(**conftest.file('pc-components.db')['device']) # Create a new transient non-db object pc1 = Desktop(
**conftest.file('pc-components.db')['device']) # Create a new transient non-db object
pc1.tags.add(Tag(id='foo-1')) pc1.tags.add(Tag(id='foo-1'))
pc1.tags.add(Tag(id='foo-2')) pc1.tags.add(Tag(id='foo-2'))
with raises(MismatchBetweenTags): with raises(MismatchBetweenTags):
@ -349,7 +352,8 @@ def test_sync_execute_register_mismatch_between_tags_and_hid():
db.session.add(Tag(id='foo-2', device=pc2)) db.session.add(Tag(id='foo-2', device=pc2))
db.session.commit() db.session.commit()
pc1 = Desktop(**conftest.file('pc-components.db')['device']) # Create a new transient non-db object pc1 = Desktop(
**conftest.file('pc-components.db')['device']) # Create a new transient non-db object
pc1.tags.add(Tag(id='foo-2')) pc1.tags.add(Tag(id='foo-2'))
with raises(MismatchBetweenTagsAndHid): with raises(MismatchBetweenTagsAndHid):
Sync().execute_register(pc1) Sync().execute_register(pc1)

View File

@ -1,3 +1,5 @@
import pathlib
import pytest import pytest
from pytest import raises from pytest import raises
from teal.db import MultipleResourcesFound, ResourceNotFound, UniqueViolation from teal.db import MultipleResourcesFound, ResourceNotFound, UniqueViolation
@ -40,7 +42,10 @@ def test_create_tag_default_org():
def test_create_tag_no_slash(): def test_create_tag_no_slash():
"""Checks that no tags can be created that contain a slash.""" """Checks that no tags can be created that contain a slash."""
with raises(ValidationError): with raises(ValidationError):
Tag(id='/') Tag('/')
with raises(ValidationError):
Tag('bar', secondary='/')
@pytest.mark.usefixtures(conftest.app_context.__name__) @pytest.mark.usefixtures(conftest.app_context.__name__)
@ -60,7 +65,7 @@ def test_create_two_same_tags():
def test_tag_post(app: Devicehub, user: UserClient): def test_tag_post(app: Devicehub, user: UserClient):
"""Checks the POST method of creating a tag.""" """Checks the POST method of creating a tag."""
user.post(res=Tag, query=[('ids', 'foo')], data={}) user.post({'id': 'foo'}, res=Tag)
with app.app_context(): with app.app_context():
assert Tag.query.filter_by(id='foo').one() assert Tag.query.filter_by(id='foo').one()
@ -70,16 +75,14 @@ def test_tag_post_etag(user: UserClient):
Ensures users cannot create eReuse.org tags through POST; Ensures users cannot create eReuse.org tags through POST;
only terminal. only terminal.
""" """
user.post(res=Tag, query=[('ids', 'FO-123456')], data={}, status=CannotCreateETag) user.post({'id': 'FO-123456'}, res=Tag, status=CannotCreateETag)
# Although similar, these are not eTags and should pass # Although similar, these are not eTags and should pass
user.post(res=Tag, query=[ user.post({'id': 'FO-0123-45'}, res=Tag)
('ids', 'FO-0123-45'), user.post({'id': 'FOO012345678910'}, res=Tag)
('ids', 'FOO012345678910'), user.post({'id': 'FO'}, res=Tag)
('ids', 'FO'), user.post({'id': 'FO-'}, res=Tag)
('ids', 'FO-'), user.post({'id': 'FO-123'}, res=Tag)
('ids', 'FO-123'), user.post({'id': 'FOO-123456'}, res=Tag)
('ids', 'FOO-123456')
], data={})
def test_tag_get_device_from_tag_endpoint(app: Devicehub, user: UserClient): def test_tag_get_device_from_tag_endpoint(app: Devicehub, user: UserClient):
@ -125,8 +128,38 @@ def test_tag_get_device_from_tag_endpoint_multiple_tags(app: Devicehub, user: Us
def test_tag_create_tags_cli(app: Devicehub, user: UserClient): def test_tag_create_tags_cli(app: Devicehub, user: UserClient):
"""Checks creating tags with the CLI endpoint.""" """Checks creating tags with the CLI endpoint."""
runner = app.test_cli_runner() runner = app.test_cli_runner()
runner.invoke(args=['create-tags', 'id1'], catch_exceptions=False) runner.invoke(args=['create-tag', 'id1'], catch_exceptions=False)
with app.app_context(): with app.app_context():
tag = Tag.query.one() # type: Tag tag = Tag.query.one() # type: Tag
assert tag.id == 'id1' assert tag.id == 'id1'
assert tag.org.id == Organization.get_default_org_id() assert tag.org.id == Organization.get_default_org_id()
@pytest.mark.usefixtures(conftest.app_context.__name__)
def test_tag_secondary():
"""Creates and consumes tags with a secondary id."""
t = Tag('foo', secondary='bar')
db.session.add(t)
db.session.flush()
assert Tag.from_an_id('bar').one() == t
assert Tag.from_an_id('foo').one() == t
with pytest.raises(ResourceNotFound):
Tag.from_an_id('nope').one()
def test_tag_create_tags_cli_csv(app: Devicehub, user: UserClient):
"""Checks creating tags with the CLI endpoint using a CSV."""
csv = pathlib.Path(__file__).parent / 'files' / 'tags-cli.csv'
runner = app.test_cli_runner()
runner.invoke(args=['create-tags-csv', str(csv)],
catch_exceptions=False)
with app.app_context():
t1 = Tag.from_an_id('id1').one()
t2 = Tag.from_an_id('sec1').one()
assert t1 == t2
def test_tag_multiple_secondary_org(user: UserClient):
"""Ensures two secondary ids cannot be part of the same Org."""
user.post({'id': 'foo', 'secondary': 'bar'}, res=Tag)
user.post({'id': 'foo1', 'secondary': 'bar'}, res=Tag, status=UniqueViolation)

View File

@ -29,7 +29,9 @@ def test_workbench_server_condensed(user: UserClient):
)) ))
s['components'][5]['events'] = [file('workbench-server-3.erase')] s['components'][5]['events'] = [file('workbench-server-3.erase')]
# Create tags # Create tags
user.post(res=Tag, query=[('ids', t['id']) for t in s['device']['tags']], data={}) for t in s['device']['tags']:
user.post({'id': t['id']}, res=Tag)
snapshot, _ = user.post(res=em.Snapshot, data=s) snapshot, _ = user.post(res=em.Snapshot, data=s)
events = snapshot['events'] events = snapshot['events']
assert {(event['type'], event['device']) for event in events} == { assert {(event['type'], event['device']) for event in events} == {