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
This commit is contained in:
fedjo 2020-07-07 14:58:55 +02:00 committed by GitHub
parent d2d48280cb
commit d172a0e756
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 5 deletions

View File

@ -77,10 +77,12 @@ class Dummy:
runner.invoke('tag', 'add', id, runner.invoke('tag', 'add', id,
'-p', 'https://t.devicetag.io', '-p', 'https://t.devicetag.io',
'-s', sec, '-s', sec,
'-u', user1.user["id"],
'-o', org_id) '-o', org_id)
# create tag for pc-laudem # create tag for pc-laudem
runner.invoke('tag', 'add', 'tagA', runner.invoke('tag', 'add', 'tagA',
'-p', 'https://t.devicetag.io', '-p', 'https://t.devicetag.io',
'-u', user1.user["id"],
'-s', 'tagA-secondary') '-s', 'tagA-secondary')
files = tuple(Path(__file__).parent.joinpath('files').iterdir()) files = tuple(Path(__file__).parent.joinpath('files').iterdir())
print('done.') print('done.')
@ -144,7 +146,7 @@ class Dummy:
res=Lot, res=Lot,
item='{}/devices'.format(lot_user3['id']), item='{}/devices'.format(lot_user3['id']),
query=[('id', pc) for pc in itertools.islice(pcs, 11, 14)]) query=[('id', pc) for pc in itertools.islice(pcs, 11, 14)])
lot4, _ = user4.post({}, lot4, _ = user4.post({},
res=Lot, res=Lot,
item='{}/devices'.format(lot_user4['id']), item='{}/devices'.format(lot_user4['id']),

View File

@ -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()}')

View File

@ -18,6 +18,7 @@ class TagDef(Resource):
VIEW = TagView VIEW = TagView
ID_CONVERTER = Converters.lower 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. ' ORG_H = 'The name of an existing organization in the DB. '
'By default the organization operating this Devicehub.' 'By default the organization operating this Devicehub.'
PROV_H = 'The Base URL of the provider; scheme + domain. Ex: "https://foo.com". ' 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, view_func=device_view,
methods={'PUT'}) methods={'PUT'})
@option('-u', '--owner', help=OWNER_H)
@option('-o', '--org', help=ORG_H) @option('-o', '--org', help=ORG_H)
@option('-p', '--provider', help=PROV_H) @option('-p', '--provider', help=PROV_H)
@option('-s', '--sec', help=Tag.secondary.comment) @option('-s', '--sec', help=Tag.secondary.comment)
@ -55,18 +57,19 @@ class TagDef(Resource):
def create_tag(self, def create_tag(self,
id: str, id: str,
org: str = None, org: str = None,
owner: str = None,
sec: str = None, sec: str = None,
provider: str = None): provider: str = None):
"""Create a tag with the given ID.""" """Create a tag with the given ID."""
db.session.add(Tag(**self.schema.load( 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() db.session.commit()
@option('--org', help=ORG_H) @option('--org', help=ORG_H)
@option('--provider', help=PROV_H) @option('--provider', help=PROV_H)
@argument('path', type=cli.Path(writable=True)) @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. """Creates tags by reading CSV from ereuse-tag.
CSV must have the following columns: CSV must have the following columns:
@ -77,6 +80,6 @@ class TagDef(Resource):
with path.open() as f: with path.open() as f:
for id, sec in csv.reader(f): for id, sec in csv.reader(f):
db.session.add(Tag(**self.schema.load( 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() db.session.commit()

View File

@ -1,6 +1,7 @@
from contextlib import suppress from contextlib import suppress
from typing import Set from typing import Set
from flask import g
from boltons import urlutils from boltons import urlutils
from sqlalchemy import BigInteger, Column, ForeignKey, UniqueConstraint from sqlalchemy import BigInteger, Column, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID 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.db import db
from ereuse_devicehub.resources.agent.models import Organization 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.user.models import User
from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.models import Thing
@ -26,6 +28,11 @@ class Tags(Set['Tag']):
class Tag(Thing): class Tag(Thing):
id = Column(db.CIText(), primary_key=True) id = Column(db.CIText(), primary_key=True)
id.comment = """The ID of the tag.""" 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), org_id = Column(UUID(as_uuid=True),
ForeignKey(Organization.id), ForeignKey(Organization.id),
primary_key=True, primary_key=True,
@ -50,7 +57,7 @@ class Tag(Thing):
primaryjoin=Device.id == device_id) primaryjoin=Device.id == device_id)
"""The device linked to this tag.""" """The device linked to this tag."""
secondary = Column(db.CIText(), index=True) 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. It has the same constraints as the main one. Only needed in special cases.
""" """

View File

@ -3,6 +3,7 @@ from sqlalchemy.util import OrderedSet
from teal.marshmallow import SanitizedStr, URL from teal.marshmallow import SanitizedStr, URL
from ereuse_devicehub.marshmallow import NestedOn 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.agent.schemas import Organization
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
@ -22,6 +23,7 @@ class Tag(Thing):
provider = URL(description=m.Tag.provider.comment, provider = URL(description=m.Tag.provider.comment,
validator=without_slash) validator=without_slash)
device = NestedOn(Device, dump_only=True) device = NestedOn(Device, dump_only=True)
owner = NestedOn(User, only_query='id')
org = NestedOn(Organization, collection_class=OrderedSet, only_query='id') org = NestedOn(Organization, collection_class=OrderedSet, only_query='id')
secondary = SanitizedStr(lower=True, description=m.Tag.secondary.comment) secondary = SanitizedStr(lower=True, description=m.Tag.secondary.comment)
printable = Boolean(dump_only=True, decsription=m.Tag.printable.__doc__) printable = Boolean(dump_only=True, decsription=m.Tag.printable.__doc__)

View File

@ -4,12 +4,14 @@ from teal.marshmallow import ValidationError
from teal.resource import View, url_for_resource from teal.resource import View, url_for_resource
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub import auth
from ereuse_devicehub.query import things_response from ereuse_devicehub.query import things_response
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):
@auth.Auth.requires_auth
def post(self): def post(self):
"""Creates a tag.""" """Creates a tag."""
num = request.args.get('num', type=int) num = request.args.get('num', type=int)
@ -19,8 +21,10 @@ class TagView(View):
res = self._post_one() res = self._post_one()
return res return res
@auth.Auth.requires_auth
def find(self, args: dict): def find(self, args: dict):
tags = Tag.query.filter(Tag.is_printable_q()) \ tags = Tag.query.filter(Tag.is_printable_q()) \
.filter_by(owner=g.user) \
.order_by(Tag.created.desc()) \ .order_by(Tag.created.desc()) \
.paginate(per_page=200) # type: Pagination .paginate(per_page=200) # type: Pagination
return things_response( return things_response(