From d172a0e756f7614590ccc5fb0492f1491425a931 Mon Sep 17 00:00:00 2001 From: fedjo Date: Tue, 7 Jul 2020 14:58:55 +0200 Subject: [PATCH] Tag-User relationship (#43) * Add owner_id reference in tag model and related migration * Add owner param to views and cli commands and schema * Create tag which belong to an owner from dummy script --- ereuse_devicehub/dummy/dummy.py | 4 +- .../versions/b9b0ee7d9dca_owner_in_tags.py | 41 +++++++++++++++++++ ereuse_devicehub/resources/tag/__init__.py | 9 ++-- ereuse_devicehub/resources/tag/model.py | 9 +++- ereuse_devicehub/resources/tag/schema.py | 2 + ereuse_devicehub/resources/tag/view.py | 4 ++ 6 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 ereuse_devicehub/migrations/versions/b9b0ee7d9dca_owner_in_tags.py diff --git a/ereuse_devicehub/dummy/dummy.py b/ereuse_devicehub/dummy/dummy.py index 4f447223..96b99785 100644 --- a/ereuse_devicehub/dummy/dummy.py +++ b/ereuse_devicehub/dummy/dummy.py @@ -77,10 +77,12 @@ class Dummy: runner.invoke('tag', 'add', id, '-p', 'https://t.devicetag.io', '-s', sec, + '-u', user1.user["id"], '-o', org_id) # create tag for pc-laudem runner.invoke('tag', 'add', 'tagA', '-p', 'https://t.devicetag.io', + '-u', user1.user["id"], '-s', 'tagA-secondary') files = tuple(Path(__file__).parent.joinpath('files').iterdir()) print('done.') @@ -144,7 +146,7 @@ class Dummy: res=Lot, item='{}/devices'.format(lot_user3['id']), query=[('id', pc) for pc in itertools.islice(pcs, 11, 14)]) - + lot4, _ = user4.post({}, res=Lot, item='{}/devices'.format(lot_user4['id']), diff --git a/ereuse_devicehub/migrations/versions/b9b0ee7d9dca_owner_in_tags.py b/ereuse_devicehub/migrations/versions/b9b0ee7d9dca_owner_in_tags.py new file mode 100644 index 00000000..5540a874 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/b9b0ee7d9dca_owner_in_tags.py @@ -0,0 +1,41 @@ +"""Owner in tags + +Revision ID: b9b0ee7d9dca +Revises: 151253ac5c55 +Create Date: 2020-06-30 17:41:28.611314 + +""" +from alembic import op +from alembic import context +import sqlalchemy as sa +import sqlalchemy_utils +from sqlalchemy.dialects import postgresql +import citext +import teal + + +# revision identifiers, used by Alembic. +revision = 'b9b0ee7d9dca' +down_revision = 'fbb7e2a0cde0' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + +def upgrade(): + op.add_column('tag', sa.Column('owner_id', postgresql.UUID(), nullable=True), schema=f'{get_inv()}') + op.create_foreign_key("fk_tag_owner_id_user_id", + "tag", "user", + ["owner_id"], ["id"], + ondelete="SET NULL", + source_schema=f'{get_inv()}', referent_schema='common') + + +def downgrade(): + op.drop_constraint("fk_tag_owner_id_user_id", "tag", type_="foreignkey", schema=f'{get_inv()}') + op.drop_column('tag', 'owner_id', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/resources/tag/__init__.py b/ereuse_devicehub/resources/tag/__init__.py index 816d89c9..1d8c75b2 100644 --- a/ereuse_devicehub/resources/tag/__init__.py +++ b/ereuse_devicehub/resources/tag/__init__.py @@ -18,6 +18,7 @@ class TagDef(Resource): VIEW = TagView ID_CONVERTER = Converters.lower + OWNER_H = 'The id of the user who owns this tag. ' 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; scheme + domain. Ex: "https://foo.com". ' @@ -48,6 +49,7 @@ class TagDef(Resource): view_func=device_view, methods={'PUT'}) + @option('-u', '--owner', help=OWNER_H) @option('-o', '--org', help=ORG_H) @option('-p', '--provider', help=PROV_H) @option('-s', '--sec', help=Tag.secondary.comment) @@ -55,18 +57,19 @@ class TagDef(Resource): def create_tag(self, id: str, org: str = None, + owner: str = None, sec: str = None, provider: str = None): """Create a tag with the given ID.""" db.session.add(Tag(**self.schema.load( - dict(id=id, org=org, secondary=sec, provider=provider) + dict(id=id, owner=owner, org=org, secondary=sec, provider=provider) ))) db.session.commit() @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): + def create_tags_csv(self, path: pathlib.Path, owner: str, org: str, provider: str): """Creates tags by reading CSV from ereuse-tag. CSV must have the following columns: @@ -77,6 +80,6 @@ class TagDef(Resource): 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) + dict(id=id, owner=owner, org=org, secondary=sec, provider=provider) ))) db.session.commit() diff --git a/ereuse_devicehub/resources/tag/model.py b/ereuse_devicehub/resources/tag/model.py index 81b331e4..3c7cbfb9 100644 --- a/ereuse_devicehub/resources/tag/model.py +++ b/ereuse_devicehub/resources/tag/model.py @@ -1,6 +1,7 @@ from contextlib import suppress from typing import Set +from flask import g from boltons import urlutils from sqlalchemy import BigInteger, Column, ForeignKey, UniqueConstraint from sqlalchemy.dialects.postgresql import UUID @@ -12,6 +13,7 @@ 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.user.models import User from ereuse_devicehub.resources.models import Thing @@ -26,6 +28,11 @@ class Tags(Set['Tag']): class Tag(Thing): id = Column(db.CIText(), primary_key=True) id.comment = """The ID of the tag.""" + owner_id = Column(UUID(as_uuid=True), + ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id) + owner = relationship(User, primaryjoin=owner_id == User.id) org_id = Column(UUID(as_uuid=True), ForeignKey(Organization.id), primary_key=True, @@ -50,7 +57,7 @@ class Tag(Thing): primaryjoin=Device.id == device_id) """The device linked to this tag.""" secondary = Column(db.CIText(), index=True) - secondary.comment = """A secondary identifier for this tag. + secondary.comment = """A secondary identifier for this tag. It has the same constraints as the main one. Only needed in special cases. """ diff --git a/ereuse_devicehub/resources/tag/schema.py b/ereuse_devicehub/resources/tag/schema.py index 8c2c355b..74dd351e 100644 --- a/ereuse_devicehub/resources/tag/schema.py +++ b/ereuse_devicehub/resources/tag/schema.py @@ -3,6 +3,7 @@ from sqlalchemy.util import OrderedSet from teal.marshmallow import SanitizedStr, URL from ereuse_devicehub.marshmallow import NestedOn +from ereuse_devicehub.resources.user.schemas import User from ereuse_devicehub.resources.agent.schemas import Organization from ereuse_devicehub.resources.device.schemas import Device from ereuse_devicehub.resources.schemas import Thing @@ -22,6 +23,7 @@ class Tag(Thing): provider = URL(description=m.Tag.provider.comment, validator=without_slash) device = NestedOn(Device, dump_only=True) + owner = NestedOn(User, only_query='id') org = NestedOn(Organization, collection_class=OrderedSet, only_query='id') secondary = SanitizedStr(lower=True, description=m.Tag.secondary.comment) printable = Boolean(dump_only=True, decsription=m.Tag.printable.__doc__) diff --git a/ereuse_devicehub/resources/tag/view.py b/ereuse_devicehub/resources/tag/view.py index af8add44..00feaad4 100644 --- a/ereuse_devicehub/resources/tag/view.py +++ b/ereuse_devicehub/resources/tag/view.py @@ -4,12 +4,14 @@ from teal.marshmallow import ValidationError from teal.resource import View, url_for_resource from ereuse_devicehub.db import db +from ereuse_devicehub import auth from ereuse_devicehub.query import things_response from ereuse_devicehub.resources.device.models import Device from ereuse_devicehub.resources.tag import Tag class TagView(View): + @auth.Auth.requires_auth def post(self): """Creates a tag.""" num = request.args.get('num', type=int) @@ -19,8 +21,10 @@ class TagView(View): res = self._post_one() return res + @auth.Auth.requires_auth def find(self, args: dict): tags = Tag.query.filter(Tag.is_printable_q()) \ + .filter_by(owner=g.user) \ .order_by(Tag.created.desc()) \ .paginate(per_page=200) # type: Pagination return things_response(