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:
parent
d2d48280cb
commit
d172a0e756
|
@ -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']),
|
||||||
|
|
|
@ -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()}')
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Reference in New Issue