Repository: virtool/virtool
Issue: Reference User Identicon

Currently, `reference.detail.users` array does not return the user's identicon field, whereas the `users.list.documents` array entries do. 
When rendering the reference's assigned user listings, the identicon field is copied over from its corresponding `users.list.documents` entry.
Assigning a new user to a reference results in a PATCH request that will add the identicon field to the new `reference.detail.users` array entry.

This should be done on the server!

Changed files:
virtool/api/references.py, virtool/db/users.py, tests/api/test_references.py

diff --git a/tests/api/test_references.py b/tests/api/test_references.py
index 223570d1a..adee0e5aa 100644
--- a/tests/api/test_references.py
+++ b/tests/api/test_references.py
@@ -386,7 +386,8 @@ async def test_add_group_or_user(error, field, spawn_client, check_ref_right, re
         })
 
         await client.db.users.insert_one({
-            "_id": "fred"
+            "_id": "fred",
+            "identicon": "foo_identicon"
         })
 
     # Don't insert the ref document if we want to trigger a 404.
@@ -418,7 +419,7 @@ async def test_add_group_or_user(error, field, spawn_client, check_ref_right, re
 
     assert resp.status == 201
 
-    assert await resp.json() == {
+    expected = {
         "id": "tech" if field == "group" else "fred",
         "created_at": static_time.iso,
         "build": False,
@@ -427,6 +428,11 @@ async def test_add_group_or_user(error, field, spawn_client, check_ref_right, re
         "remove": False
     }
 
+    if field == "user":
+        expected["identicon"] = "foo_identicon"
+
+    assert await resp.json() == expected
+
 
 @pytest.mark.parametrize("error", [None, "404_field", "404_ref"])
 @pytest.mark.parametrize("field", ["group", "user"])
@@ -459,6 +465,11 @@ async def test_edit_group_or_user(error, field, spawn_client, check_ref_right, r
     if error != "404_ref":
         await client.db.references.insert_one(document)
 
+    await client.db.users.insert_one({
+        "_id": "fred",
+        "identicon": "foo_identicon"
+    })
+
     subdocument_id = "tech" if field == "group" else "fred"
 
     url = "/api/refs/foo/{}s/{}".format(field, subdocument_id)
@@ -477,7 +488,7 @@ async def test_edit_group_or_user(error, field, spawn_client, check_ref_right, r
 
     assert resp.status == 200
 
-    assert await resp.json() == {
+    expected = {
         "id": subdocument_id,
         "build": False,
         "modify": False,
@@ -485,6 +496,11 @@ async def test_edit_group_or_user(error, field, spawn_client, check_ref_right, r
         "remove": True
     }
 
+    if field == "user":
+        expected["identicon"] = "foo_identicon"
+
+    assert await resp.json() == expected
+
     assert await client.db.references.find_one() == {
         "_id": "foo",
         "groups": [{
diff --git a/virtool/api/references.py b/virtool/api/references.py
index f0a8599c7..9ba3c2576 100644
--- a/virtool/api/references.py
+++ b/virtool/api/references.py
@@ -9,6 +9,7 @@ import virtool.db.indexes
 import virtool.db.otus
 import virtool.db.processes
 import virtool.db.references
+import virtool.db.users
 import virtool.db.utils
 import virtool.errors
 import virtool.github
@@ -96,6 +97,8 @@ async def get(req):
 
     document.update(await virtool.db.references.get_computed(db, ref_id, internal_control_id))
 
+    document["users"] = await virtool.db.users.attach_identicons(db, document["users"])
+
     return json_response(virtool.utils.base_processor(document))
 
 
@@ -515,6 +518,8 @@ async def edit(req):
             }
         })
 
+    document["users"] = await virtool.db.users.attach_identicons(db, document["users"])
+
     return json_response(document)
 
 
@@ -657,6 +662,8 @@ async def add_user(req):
         "Location": "/api/refs/{}/users/{}".format(ref_id, subdocument["id"])
     }
 
+    subdocument = await virtool.db.users.attach_identicons(db, subdocument)
+
     return json_response(subdocument, headers=headers, status=201)
 
 
@@ -700,6 +707,8 @@ async def edit_user(req):
     if subdocument is None:
         return not_found()
 
+    subdocument = await virtool.db.users.attach_identicons(db, subdocument)
+
     return json_response(subdocument)
 
 
@@ -717,7 +726,7 @@ async def delete_group(req):
     if not await virtool.db.references.check_right(req, ref_id, "modify"):
         return insufficient_rights()
 
-    deleted_id = await virtool.db.references.delete_group_or_user(db, ref_id, group_id, "groups")
+    await virtool.db.references.delete_group_or_user(db, ref_id, group_id, "groups")
 
     return no_content()
 
@@ -736,9 +745,6 @@ async def delete_user(req):
     if not await virtool.db.references.check_right(req, ref_id, "modify"):
         return insufficient_rights()
 
-    deleted_id = await virtool.db.references.delete_group_or_user(db, ref_id, user_id, "users")
-
-    if not deleted_id:
-        return not_found()
+    await virtool.db.references.delete_group_or_user(db, ref_id, user_id, "users")
 
     return no_content()
diff --git a/virtool/db/users.py b/virtool/db/users.py
index 2a9d79503..1d0dd488e 100644
--- a/virtool/db/users.py
+++ b/virtool/db/users.py
@@ -1,3 +1,5 @@
+from typing import Union
+
 import virtool.db.groups
 import virtool.db.utils
 import virtool.errors
@@ -28,6 +30,33 @@ ACCOUNT_PROJECTION = [
 ]
 
 
+async def attach_identicons(db, users: Union[dict, list]):
+    """
+    Attach identicon fields to a list of user documents.
+
+    :param db: the application database client
+    :type db: :class:`~motor.motor_asyncio.AsyncIOMotorClient`
+
+    :param users: user documents to attach identicons to
+
+    :return: the user documents with identicon fields
+    :rtype: Union[dict,list]
+
+    """
+    if isinstance(users, list):
+        user_ids = [u["id"] for u in users]
+
+        cursor = db.users.find({"_id": {"$in": user_ids}}, ["identicon"])
+
+        lookup = {d["_id"]: d["identicon"] async for d in cursor}
+
+        return [dict(u, identicon=lookup[u["id"]]) for u in users]
+
+    identicon = await virtool.db.utils.get_one_field(db.users, "identicon", users["id"])
+
+    return dict(users, identicon=identicon)
+
+
 def compose_force_reset_update(force_reset):
     """
     Compose a update dict for the database given a `force_reset` value.