Add secondary id to tags
This commit is contained in:
parent
543e457581
commit
3b0f483a90
|
@ -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`,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
id1,sec1
|
||||||
|
id2,sec2
|
||||||
|
id3,sec3
|
||||||
|
id4,sec4
|
|
|
@ -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='/')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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} == {
|
||||||
|
|
Reference in New Issue