diff --git a/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py index 50a803a9..17e0290e 100644 --- a/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py +++ b/ereuse_devicehub/migrations/versions/6a2a939d5668_drop_unique_org_for_tag.py @@ -23,13 +23,34 @@ def get_inv(): raise ValueError("Inventory value is not specified") return INV + +def upgrade_data(): + con = op.get_bind() + tags = con.execute(f"select id from {get_inv()}.tag") + i = 1 + for c in tags: + id_tag = c.id + internal_id = i + i += 1 + sql = f"update {get_inv()}.tag set internal_id='{internal_id}' where id='{id_tag}';" + con.execute(sql) + + sql = f"CREATE SEQUENCE {get_inv()}.tag_internal_id_seq START {i};" + con.execute(sql) + + def upgrade(): op.drop_constraint('one tag id per organization', 'tag', schema=f'{get_inv()}') op.drop_constraint('one secondary tag per organization', 'tag', schema=f'{get_inv()}') op.create_primary_key('one tag id per owner', 'tag', ['id', 'owner_id'], schema=f'{get_inv()}'), op.create_unique_constraint('one secondary tag per owner', 'tag', ['secondary', 'owner_id'], schema=f'{get_inv()}'), - op.add_column('tag', sa.Column('internal_id', sa.BigInteger(), nullable=False, - comment='The identifier of the tag for this database. Used only\n internally for software; users should not use this.\n'), schema=f'{get_inv()}') + op.add_column('tag', sa.Column('internal_id', sa.BigInteger(), nullable=True, + comment='The identifier of the tag for this database. Used only\n internally for software; users should not use this.\n'), schema=f'{get_inv()}') + + upgrade_data() + + op.alter_column('tag', sa.Column('internal_id', sa.BigInteger(), nullable=False, + comment='The identifier of the tag for this database. Used only\n internally for software; users should not use this.\n'), schema=f'{get_inv()}') def downgrade(): @@ -38,5 +59,4 @@ def downgrade(): op.create_primary_key('one tag id per organization', 'tag', ['id', 'org_id'], schema=f'{get_inv()}'), op.create_unique_constraint('one secondary tag per organization', 'tag', ['secondary', 'org_id'], schema=f'{get_inv()}'), op.drop_column('tag', 'internal_id', schema=f'{get_inv()}') - op.drop_column('tag', 'internal_id', schema=f'{get_inv()}') op.execute(f"DROP SEQUENCE {get_inv()}.tag_internal_id_seq;") diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 757f02ae..c52cdf8a 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -15,6 +15,7 @@ from ereuse_devicehub.resources.agent.models import Organization from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.resources.utils import hascode class Tags(Set['Tag']): @@ -25,8 +26,10 @@ class Tags(Set['Tag']): return ', '.join(format(tag, format_spec) for tag in self).strip() + + class Tag(Thing): - internal_id = Column(BigInteger, Sequence('tag_internal_id_seq'), unique=True, nulable=False) + internal_id = Column(BigInteger, Sequence('tag_internal_id_seq'), unique=True, nullable=False) internal_id.comment = """The identifier of the tag for this database. Used only internally for software; users should not use this. """ @@ -113,7 +116,7 @@ class Tag(Thing): def url(self) -> urlutils.URL: """The URL where to GET this device.""" # todo this url only works for printable internal tags - return urlutils.URL(url_for_resource(Tag, item_id=self.id)) + return urlutils.URL(url_for_resource(Tag, item_id=self.code)) @property def printable(self) -> bool: @@ -129,6 +132,10 @@ class Tag(Thing): """Return a SQLAlchemy filter expression for printable queries.""" return cls.org_id == Organization.get_default_org_id() + @property + def code(self) -> str: + return hascode.encode(self.internal_id) + def delete(self): """Deletes the tag. @@ -157,7 +164,7 @@ class TagLinked(ValidationError): message = 'The tag {} is linked to device {}.'.format(tag.id, tag.device.id) super().__init__(message, field_names=['device']) - + class TagUnnamed(ValidationError): def __init__(self, id): message = 'This tag {} is unnamed tag. It is imposible delete.'.format(id) diff --git a/ereuse_devicehub/resources/tag/schema.py b/ereuse_devicehub/resources/tag/schema.py index 7db4fe0d..e1c8b608 100644 --- a/ereuse_devicehub/resources/tag/schema.py +++ b/ereuse_devicehub/resources/tag/schema.py @@ -28,3 +28,4 @@ class Tag(Thing): secondary = SanitizedStr(lower=True, description=m.Tag.secondary.comment) printable = Boolean(dump_only=True, decsription=m.Tag.printable.__doc__) url = URL(dump_only=True, description=m.Tag.url.__doc__) + code = SanitizedStr(dump_only=True, description=m.Tag.internal_id.comment) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index 75f575f5..5416fd2a 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -6,14 +6,16 @@ from teal.resource import View, url_for_resource from ereuse_devicehub import auth from ereuse_devicehub.db import db from ereuse_devicehub.query import things_response +from ereuse_devicehub.resources.utils import hascode from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.tag import Tag class TagView(View): - def one(self, id): + def one(self, code): """Gets the device from the named tag, /tags/namedtag.""" - tag = Tag.from_an_id(id).one() # type: Tag + internal_id = hascode.decode(code.upper()) or -1 + tag = Tag.query.filter_by(internal_id=internal_id).one() # type: Tag if not tag.device: raise TagNotLinked(tag.id) return redirect(location=url_for_resource(Device, tag.device.id)) diff --git a/ereuse_devicehub/resources/utils.py b/ereuse_devicehub/resources/utils.py new file mode 100644 index 00000000..1ac37b51 --- /dev/null +++ b/ereuse_devicehub/resources/utils.py @@ -0,0 +1,9 @@ +from hashids import Hashids +from decouple import config + +ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' +SECRET = config('TAG_HASH', '') +hascode = Hashids(SECRET, min_length=5, alphabet=ALPHABET) + +def hashids(id): + return hascode.encode(id) diff --git a/tests/test_tag.py b/tests/test_tag.py index ce76194c..040b2751 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -26,6 +26,7 @@ from tests.conftest import file @pytest.mark.usefixtures(conftest.app_context.__name__) def test_create_tag(user: UserClient): """Creates a tag specifying a custom organization.""" + # import pdb; pdb.set_trace() org = Organization(name='bar', tax_id='bartax') tag = Tag(id='bar-1', org=org, provider=URL('http://foo.bar'), owner_id=user.user['id']) db.session.add(tag) @@ -33,7 +34,7 @@ def test_create_tag(user: UserClient): tag = Tag.query.one() assert tag.id == 'bar-1' assert tag.provider == URL('http://foo.bar') - res, _ = user.get(res=Tag, item=tag.id, status=422) + res, _ = user.get(res=Tag, item=tag.code, status=422) assert res['type'] == 'TagNotLinked'