Skip to content

Commit 4c0eae7

Browse files
Ken LippoldKen Lippold
authored andcommitted
Added get_tag_keys endpoint
1 parent 2ff1334 commit 4c0eae7

File tree

7 files changed

+145
-56
lines changed

7 files changed

+145
-56
lines changed

sta/models/tag.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import typing
2+
from typing import Optional
23
from django.db import models
4+
from django.db.models import Q
35
from iam.models.utils import PermissionChecker
46
from .thing import Thing
57

@@ -9,7 +11,26 @@
911
User = get_user_model()
1012

1113

14+
class TagQuerySet(models.QuerySet):
15+
def visible(self, user: Optional["User"]):
16+
if user is None:
17+
return self.filter(Q(thing__workspace__is_private=False, thing__is_private=False))
18+
elif user.account_type == "admin":
19+
return self
20+
else:
21+
return self.filter(
22+
Q(thing__workspace__is_private=False,
23+
thing__is_private=False) |
24+
Q(thing__workspace__owner=user) |
25+
Q(thing__workspace__collaborators__user=user,
26+
thing__workspace__collaborators__role__permissions__resource_type__in=["*", "Thing"],
27+
thing__workspace__collaborators__role__permissions__permission_type__in=["*", "view"])
28+
)
29+
30+
1231
class Tag(models.Model, PermissionChecker):
1332
thing = models.ForeignKey(Thing, related_name="tags", on_delete=models.DO_NOTHING)
1433
key = models.CharField(max_length=255)
1534
value = models.CharField(max_length=255)
35+
36+
objects = TagQuerySet.as_manager()

sta/services/thing.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ def get_tags(self, user: Optional[User], uid: uuid.UUID):
9797

9898
return thing.tags
9999

100+
@staticmethod
101+
def get_tag_keys(user: Optional[User], workspace_id: Optional[uuid.UUID], thing_id: Optional[uuid.UUID]):
102+
queryset = Tag.objects
103+
104+
if workspace_id:
105+
queryset = queryset.filter(thing__workspace_id=workspace_id)
106+
107+
if thing_id:
108+
queryset = queryset.filter(thing_id=thing_id)
109+
110+
return list(queryset.visible(user=user).values_list("key", flat=True).distinct())
111+
100112
def add_tag(self, user: User, uid: uuid.UUID, data: TagPostBody):
101113
thing = self.get_thing_for_action(user=user, uid=uid, action="edit")
102114

sta/urls.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from django.urls import path
33
from django.views.decorators.csrf import ensure_csrf_cookie
44
from hydroserver import __version__
5-
from sta.views import (thing_router, tag_router, photo_router, observed_property_router, processing_level_router,
6-
result_qualifier_router, sensor_router, unit_router, datastream_router)
5+
from sta.views import (thing_router, tag_router, tag_key_router, photo_router, observed_property_router,
6+
processing_level_router, result_qualifier_router, sensor_router, unit_router, datastream_router)
77

88

99
data_api = NinjaAPI(
@@ -16,6 +16,7 @@
1616
thing_router.add_router("{thing_id}/tags", tag_router)
1717
thing_router.add_router("{thing_id}/photos", photo_router)
1818
data_api.add_router("things", thing_router)
19+
data_api.add_router("tags", tag_key_router)
1920
data_api.add_router("observed-properties", observed_property_router)
2021
data_api.add_router("processing-levels", processing_level_router)
2122
data_api.add_router("result-qualifiers", result_qualifier_router)

sta/views/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .thing import thing_router
2-
from .tag import tag_router
2+
from .tag import tag_router, tag_key_router
33
from .photo import photo_router
44
from .observed_property import observed_property_router
55
from .processing_level import processing_level_router

sta/views/tag.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import uuid
22
from ninja import Router, Path
3+
from typing import Optional
34
from hydroserver.security import bearer_auth, session_auth, anonymous_auth
45
from hydroserver.http import HydroServerHttpRequest
56
from sta.schemas import TagGetResponse, TagPostBody, TagDeleteBody
67
from sta.services import ThingService
78

89
tag_router = Router(tags=["Tags"])
10+
tag_key_router = Router(tags=["Tags"])
911
thing_service = ThingService()
1012

1113

@@ -30,6 +32,27 @@ def get_tags(request: HydroServerHttpRequest, thing_id: Path[uuid.UUID]):
3032
)
3133

3234

35+
@tag_key_router.get(
36+
"keys",
37+
auth=[session_auth, bearer_auth, anonymous_auth],
38+
response={
39+
200: list[str],
40+
401: str,
41+
}
42+
)
43+
def get_tag_keys(request: HydroServerHttpRequest, workspace_id: Optional[uuid.UUID] = None,
44+
thing_id: Optional[uuid.UUID] = None):
45+
"""
46+
Get all existing unique tag keys.
47+
"""
48+
49+
return 200, thing_service.get_tag_keys(
50+
user=request.authenticated_user,
51+
workspace_id=workspace_id,
52+
thing_id=thing_id,
53+
)
54+
55+
3356
@tag_router.post(
3457
"",
3558
auth=[session_auth, bearer_auth],

tests/fixtures/test_things.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
pk: 1000000010
2727
fields:
2828
thing: 3b7818af-eff7-4149-8517-e5cad9dc22e1
29-
key: Test Key
30-
value: Test Value
29+
key: Test Public Key
30+
value: Test Public Value
3131
- model: sta.photo
3232
pk: 1000000010
3333
fields:
@@ -63,8 +63,8 @@
6363
pk: 1000000011
6464
fields:
6565
thing: 76dadda5-224b-4e1f-8570-e385bd482b2d
66-
key: Test Key
67-
value: Test Value
66+
key: Test Private Key
67+
value: Test Private Value
6868
- model: sta.photo
6969
pk: 1000000011
7070
fields:
@@ -100,8 +100,8 @@
100100
pk: 1000000012
101101
fields:
102102
thing: 92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7
103-
key: Test Key
104-
value: Test Value
103+
key: Test Private Key
104+
value: Test Private Value
105105
- model: sta.photo
106106
pk: 1000000012
107107
fields:
@@ -137,8 +137,8 @@
137137
pk: 1000000013
138138
fields:
139139
thing: 819260c8-2543-4046-b8c4-7431243ed7c5
140-
key: Test Key
141-
value: Test Value
140+
key: Test Private Key
141+
value: Test Private Value
142142
- model: sta.photo
143143
pk: 1000000013
144144
fields:

tests/sta/services/test_thing.py

Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,46 @@ def test_get_tags(get_user, user, thing, message, error_code):
234234
user=get_user(user), uid=uuid.UUID(thing)
235235
)
236236
assert len(list(thing_tags.all())) == 1
237-
assert list(thing_tags.all())[0].key == "Test Key"
238-
assert list(thing_tags.all())[0].value == "Test Value"
237+
assert list(thing_tags.all())[0].key in ["Test Public Key", "Test Private Key"]
238+
assert list(thing_tags.all())[0].value in ["Test Public Value", "Test Private Value"]
239+
240+
241+
@pytest.mark.parametrize("user, workspace, thing, length, max_queries", [
242+
("owner", None, None, 2, 2),
243+
("owner", "b27c51a0-7374-462d-8a53-d97d47176c10", None, 1, 2),
244+
("owner", None, "76dadda5-224b-4e1f-8570-e385bd482b2d", 1, 2),
245+
("owner", "6e0deaf2-a92b-421b-9ece-86783265596f", "76dadda5-224b-4e1f-8570-e385bd482b2d", 0, 2),
246+
("admin", None, None, 2, 2),
247+
("admin", "b27c51a0-7374-462d-8a53-d97d47176c10", None, 1, 2),
248+
("admin", None, "76dadda5-224b-4e1f-8570-e385bd482b2d", 1, 2),
249+
("admin", "6e0deaf2-a92b-421b-9ece-86783265596f", "76dadda5-224b-4e1f-8570-e385bd482b2d", 0, 2),
250+
("editor", None, None, 2, 2),
251+
("editor", "b27c51a0-7374-462d-8a53-d97d47176c10", None, 1, 2),
252+
("editor", None, "76dadda5-224b-4e1f-8570-e385bd482b2d", 1, 2),
253+
("editor", "6e0deaf2-a92b-421b-9ece-86783265596f", "76dadda5-224b-4e1f-8570-e385bd482b2d", 0, 2),
254+
("viewer", None, None, 2, 2),
255+
("viewer", "b27c51a0-7374-462d-8a53-d97d47176c10", None, 1, 2),
256+
("viewer", None, "76dadda5-224b-4e1f-8570-e385bd482b2d", 1, 2),
257+
("viewer", "6e0deaf2-a92b-421b-9ece-86783265596f", "76dadda5-224b-4e1f-8570-e385bd482b2d", 0, 2),
258+
("anonymous", None, None, 1, 2),
259+
("anonymous", "b27c51a0-7374-462d-8a53-d97d47176c10", None, 0, 2),
260+
("anonymous", None, "76dadda5-224b-4e1f-8570-e385bd482b2d", 0, 2),
261+
("anonymous", "6e0deaf2-a92b-421b-9ece-86783265596f", "76dadda5-224b-4e1f-8570-e385bd482b2d", 0, 2),
262+
])
263+
def test_get_tag_keys(django_assert_num_queries, get_user, user, workspace, thing, length, max_queries):
264+
with django_assert_num_queries(max_queries):
265+
tag_key_list = thing_service.get_tag_keys(
266+
user=get_user(user), workspace_id=uuid.UUID(workspace) if workspace else None,
267+
thing_id=uuid.UUID(thing) if thing else None
268+
)
269+
assert len(tag_key_list) == length
270+
assert (isinstance(str, tag_key) for tag_key in tag_key_list)
239271

240272

241273
@pytest.mark.parametrize("user, thing, tag, message, error_code", [
242274
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "New Key", "value": "New Value"}, None, None),
243275
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "New Key", "value": "Test Value"}, None, None),
244-
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key", "value": "New Value"}, "Tag already exists", 400),
276+
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key", "value": "New Value"}, "Tag already exists", 400),
245277
("owner", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "New Key", "value": "New Value"}, None, None),
246278
("owner", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "New Key", "value": "New Value"}, None, None),
247279
("owner", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "New Key", "value": "New Value"}, None, None),
@@ -280,28 +312,28 @@ def test_add_tag(get_user, user, thing, tag, message, error_code):
280312

281313

282314
@pytest.mark.parametrize("user, thing, tag, message, error_code", [
283-
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key", "value": "New Value"}, None, None),
284-
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key", "value": "New Value"}, None, None),
315+
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key", "value": "New Value"}, None, None),
316+
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key", "value": "New Value"}, None, None),
285317
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "New Key", "value": "New Value"}, "Tag does not exist", 404),
286-
("owner", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key", "value": "New Value"}, None, None),
287-
("owner", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key", "value": "New Value"}, None, None),
288-
("owner", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key", "value": "New Value"}, None, None),
289-
("admin", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key", "value": "New Value"}, None, None),
290-
("admin", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key", "value": "New Value"}, None, None),
291-
("admin", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key", "value": "New Value"}, None, None),
292-
("admin", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key", "value": "New Value"}, None, None),
293-
("editor", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key", "value": "New Value"}, None, None),
294-
("editor", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key", "value": "New Value"}, None, None),
295-
("editor", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key", "value": "New Value"}, None, None),
296-
("editor", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key", "value": "New Value"}, None, None),
297-
("viewer", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key", "value": "New Value"}, "You do not have permission", 403),
298-
("viewer", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key", "value": "New Value"}, "You do not have permission", 403),
299-
("viewer", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key", "value": "New Value"}, "You do not have permission", 403),
300-
("viewer", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key", "value": "New Value"}, "You do not have permission", 403),
301-
("anonymous", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key", "value": "New Value"}, "You do not have permission", 403),
302-
("anonymous", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key", "value": "New Value"}, "Thing does not exist", 404),
303-
("anonymous", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key", "value": "New Value"}, "Thing does not exist", 404),
304-
("anonymous", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key", "value": "New Value"}, "Thing does not exist", 404),
318+
("owner", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key", "value": "New Value"}, None, None),
319+
("owner", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key", "value": "New Value"}, None, None),
320+
("owner", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key", "value": "New Value"}, None, None),
321+
("admin", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key", "value": "New Value"}, None, None),
322+
("admin", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key", "value": "New Value"}, None, None),
323+
("admin", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key", "value": "New Value"}, None, None),
324+
("admin", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key", "value": "New Value"}, None, None),
325+
("editor", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key", "value": "New Value"}, None, None),
326+
("editor", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key", "value": "New Value"}, None, None),
327+
("editor", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key", "value": "New Value"}, None, None),
328+
("editor", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key", "value": "New Value"}, None, None),
329+
("viewer", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key", "value": "New Value"}, "You do not have permission", 403),
330+
("viewer", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key", "value": "New Value"}, "You do not have permission", 403),
331+
("viewer", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key", "value": "New Value"}, "You do not have permission", 403),
332+
("viewer", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key", "value": "New Value"}, "You do not have permission", 403),
333+
("anonymous", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key", "value": "New Value"}, "You do not have permission", 403),
334+
("anonymous", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key", "value": "New Value"}, "Thing does not exist", 404),
335+
("anonymous", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key", "value": "New Value"}, "Thing does not exist", 404),
336+
("anonymous", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key", "value": "New Value"}, "Thing does not exist", 404),
305337
])
306338
def test_update_tag(get_user, user, thing, tag, message, error_code):
307339
if error_code:
@@ -321,28 +353,28 @@ def test_update_tag(get_user, user, thing, tag, message, error_code):
321353

322354

323355
@pytest.mark.parametrize("user, thing, tag, message, error_code", [
324-
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key"}, None, None),
325-
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key"}, None, None),
356+
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key"}, None, None),
357+
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key"}, None, None),
326358
("owner", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "New Key"}, "Tag does not exist", 404),
327-
("owner", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key"}, None, None),
328-
("owner", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key"}, None, None),
329-
("owner", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key"}, None, None),
330-
("admin", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key"}, None, None),
331-
("admin", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key"}, None, None),
332-
("admin", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key"}, None, None),
333-
("admin", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key"}, None, None),
334-
("editor", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key"}, None, None),
335-
("editor", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key"}, None, None),
336-
("editor", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key"}, None, None),
337-
("editor", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key"}, None, None),
338-
("viewer", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key"}, "You do not have permission", 403),
339-
("viewer", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key"}, "You do not have permission", 403),
340-
("viewer", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key"}, "You do not have permission", 403),
341-
("viewer", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key"}, "You do not have permission", 403),
342-
("anonymous", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Key"}, "You do not have permission", 403),
343-
("anonymous", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Key"}, "Thing does not exist", 404),
344-
("anonymous", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Key"}, "Thing does not exist", 404),
345-
("anonymous", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Key"}, "Thing does not exist", 404),
359+
("owner", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key"}, None, None),
360+
("owner", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key"}, None, None),
361+
("owner", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key"}, None, None),
362+
("admin", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key"}, None, None),
363+
("admin", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key"}, None, None),
364+
("admin", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key"}, None, None),
365+
("admin", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key"}, None, None),
366+
("editor", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key"}, None, None),
367+
("editor", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key"}, None, None),
368+
("editor", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key"}, None, None),
369+
("editor", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key"}, None, None),
370+
("viewer", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key"}, "You do not have permission", 403),
371+
("viewer", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key"}, "You do not have permission", 403),
372+
("viewer", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key"}, "You do not have permission", 403),
373+
("viewer", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key"}, "You do not have permission", 403),
374+
("anonymous", "3b7818af-eff7-4149-8517-e5cad9dc22e1", {"key": "Test Public Key"}, "You do not have permission", 403),
375+
("anonymous", "76dadda5-224b-4e1f-8570-e385bd482b2d", {"key": "Test Private Key"}, "Thing does not exist", 404),
376+
("anonymous", "92a3a099-f2d3-40ec-9b0e-d25ae8bf59b7", {"key": "Test Private Key"}, "Thing does not exist", 404),
377+
("anonymous", "819260c8-2543-4046-b8c4-7431243ed7c5", {"key": "Test Private Key"}, "Thing does not exist", 404),
346378
])
347379
def test_remove_tag(get_user, user, thing, tag, message, error_code):
348380
if error_code:

0 commit comments

Comments
 (0)