Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add labels collection and endpoints #1892

Merged
merged 10 commits into from Sep 25, 2020
Empty file added tests/labels/__init__.py
Empty file.
196 changes: 196 additions & 0 deletions tests/labels/test_api.py
@@ -0,0 +1,196 @@
import pytest


async def test_find(spawn_client):
"""
Test that a ``GET /api/labels`` return a complete list of labels.

"""
client = await spawn_client(authorize=True, administrator=True)
await client.db.labels.insert_many([
{
"_id": "test_1",
"name": "Bug",
"color": "#a83432"
},
{
"_id": "test_2",
"name": "Question",
"color": "#03fc20"
}
])

resp = await client.get("/api/labels")
assert resp.status == 200

assert await resp.json() == [
{
"id": "test_1",
"name": "Bug",
"color": "#a83432"
},
{
"id": "test_2",
"name": "Question",
"color": "#03fc20"
}
]


@pytest.mark.parametrize("error", [None, "404"])
async def test_get(error, spawn_client, all_permissions, resp_is):
"""
Test that a ``GET /api/labels/:label_id`` return the correct label document.

"""
client = await spawn_client(authorize=True, administrator=True)

if not error:
await client.db.labels.insert_one({
"_id": "test",
"name": "Bug",
"color": "#a83432"
})

resp = await client.get("/api/labels/test")

if error:
assert await resp_is.not_found(resp)
return

assert resp.status == 200

assert await resp.json() == {
"id": "test",
"name": "Bug",
"color": "#a83432"
}


@pytest.mark.parametrize("error", [None, "400_exists", "422_color"])
async def test_create(error, spawn_client, test_random_alphanumeric, resp_is):
"""
Test that a label can be added to the database at ``POST /api/labels``.

"""
client = await spawn_client(authorize=True, administrator=True)

if error == "400_exists":
await client.db.labels.insert_one({
"name": "Bug"
})

data = {
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
}

if error == "422_color":
data["color"] = "#1234567"

resp = await client.post("/api/labels", data)

if error == "400_exists":
assert await resp_is.bad_request(resp, "Label name already exists")
return

if error == "422_color":
assert resp.status == 422
return

assert resp.status == 201

expected_id = test_random_alphanumeric.history[0]
assert resp.headers["Location"] == "/api/labels/" + expected_id

assert await resp.json() == {
"id": expected_id,
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
}


@pytest.mark.parametrize("error", [None, "404", "400_exists", "422_color"])
async def test_edit(error, spawn_client, resp_is):
"""
Test that a label can be edited to the database at ``PATCH /api/labels/:label_id``.

"""
client = await spawn_client(authorize=True, administrator=True)

if error != "404":
await client.db.labels.insert_many([
{
"_id": "test_1",
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
},
{
"_id": "test_2",
"name": "Question",
"color": "#32a85f",
"description": "Question from a user"
}
])

data = {
"name": "Bug",
"color": "#fc5203",
"description": "Need to be fixed"
}

if error == "400_exists":
data["name"] = "Question"

if error == "422_color":
data["color"] = "#123bzp"

resp = await client.patch("/api/labels/test_1", data=data)

if error == "404":
assert await resp_is.not_found(resp)
return

if error == "400_exists":
assert await resp_is.bad_request(resp, "Label name already exists")
return

if error == "422_color":
assert resp.status == 422
return

assert resp.status == 200
assert await resp.json() == {
"id": "test_1",
"name": "Bug",
"color": "#fc5203",
"description": "Need to be fixed"
}


@pytest.mark.parametrize("error", [None, "400"])
async def test_remove(error, spawn_client, resp_is):
"""
Test that a label can be deleted to the database at ``DELETE /api/labels/:label_id``.

"""
client = await spawn_client(authorize=True, administrator=True)

if not error:
await client.db.labels.insert_one({
"_id": "test",
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
})

resp = await client.delete("/api/labels/test")

if error:
assert await resp_is.not_found(resp)
return

assert await resp_is.no_content(resp)
2 changes: 2 additions & 0 deletions virtool/db/core.py
Expand Up @@ -299,6 +299,8 @@ def __init__(self, client, enqueue_change):
silent=True
)

self.labels = self.bind_collection("labels")

self.otus = self.bind_collection(
"otus",
projection=virtool.otus.db.PROJECTION
Expand Down
1 change: 1 addition & 0 deletions virtool/dispatcher.py
Expand Up @@ -31,6 +31,7 @@
"hmm",
"indexes",
"jobs",
"labels",
"otus",
"processes",
"references",
Expand Down
Empty file added virtool/labels/__init__.py
Empty file.
138 changes: 138 additions & 0 deletions virtool/labels/api.py
@@ -0,0 +1,138 @@
import virtool.http.routes
import virtool.utils
import virtool.validators
import virtool.db.utils
from virtool.api.response import bad_request, json_response, no_content, not_found

routes = virtool.http.routes.Routes()


@routes.get("/api/labels")
async def find(req):
"""
Get a list of all label documents in the database.

"""
db = req.app["db"]

cursor = db.labels.find()

return json_response([virtool.utils.base_processor(d) async for d in cursor])


@routes.get("/api/labels/{label_id}")
async def get(req):
"""
Get a complete label document.

"""
document = await req.app["db"].labels.find_one(req.match_info["label_id"])

if document is None:
return not_found()

return json_response(virtool.utils.base_processor(document))


@routes.post("/api/labels", schema={
"name": {
"type": "string",
"coerce": virtool.validators.strip,
"required": True,
"empty": False
},
"color": {
"type": "string",
"coerce": virtool.validators.strip,
"validator": virtool.validators.is_valid_hex_color,
},
"description": {
"type": "string",
"coerce": virtool.validators.strip,
"default": ""
}
})
async def create(req):
"""
Add a new label to the labels database.

"""
db = req.app["db"]
data = req["data"]

if await db.labels.count_documents({'name': data['name']}):
return bad_request("Label name already exists")

label_id = await virtool.db.utils.get_new_id(db.labels)

document = {
"_id": label_id,
"name": data["name"],
"color": data["color"],
"description": data["description"]
}

await db.labels.insert_one(document)

headers = {
"Location": f"/api/labels/{label_id}"
}

return json_response(virtool.utils.base_processor(document), status=201, headers=headers)


@routes.patch("/api/labels/{label_id}", schema={
"name": {
"type": "string",
"coerce": virtool.validators.strip,
},
"color": {
"type": "string",
"coerce": virtool.validators.strip,
"validator": virtool.validators.is_valid_hex_color,
},
"description": {
"type": "string",
"coerce": virtool.validators.strip,
}
})
async def edit(req):
"""
Edit an existing label.

"""
db = req.app["db"]
data = req["data"]

label_id = req.match_info["label_id"]

if "name" in data and await db.labels.count_documents({"_id": {"$ne": label_id}, "name": data["name"]}):
return bad_request("Label name already exists")

document = await db.labels.find_one_and_update({"_id": label_id}, {
"$set": data
})

if document is None:
return not_found()

return json_response(virtool.utils.base_processor(document))


@routes.delete("/api/labels/{label_id}")
async def remove(req):
"""
Remove a label.

"""
db = req.app["db"]

label_id = req.match_info["label_id"]

delete_result = await db.labels.delete_one({"_id": label_id})

if delete_result.deleted_count == 0:
return not_found()

return no_content()

2 changes: 2 additions & 0 deletions virtool/routes.py
Expand Up @@ -16,6 +16,7 @@
import virtool.http.ws
import virtool.indexes.api
import virtool.jobs.api
import virtool.labels.api
import virtool.otus.api
import virtool.processes.api
import virtool.references.api
Expand Down Expand Up @@ -54,6 +55,7 @@
virtool.hmm.api.routes,
virtool.indexes.api.routes,
virtool.jobs.api.routes,
virtool.labels.api.routes,
virtool.otus.api.routes,
virtool.processes.api.routes,
virtool.references.api.routes,
Expand Down
8 changes: 8 additions & 0 deletions virtool/validators.py
@@ -1,5 +1,8 @@
import re
import virtool.users.utils

RE_HEX_COLOR = re.compile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")


def strip(value: str) -> str:
"""
Expand All @@ -20,3 +23,8 @@ def is_permission_dict(field, value, error):
def has_unique_segment_names(field, value, error):
if len({seg["name"] for seg in value}) != len(value):
error(field, "list contains duplicate names")


def is_valid_hex_color(field, value, error):
if not RE_HEX_COLOR.match(value):
error(field, "This is not a valid Hexadecimal color")