about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--gn3/auth/authorisation/checks.py13
-rw-r--r--gn3/auth/authorisation/users/collections/__init__.py1
-rw-r--r--gn3/auth/authorisation/users/collections/models.py (renamed from gn3/auth/authorisation/users/collections.py)66
-rw-r--r--gn3/auth/authorisation/users/collections/views.py73
-rw-r--r--gn3/auth/authorisation/users/views.py35
5 files changed, 147 insertions, 41 deletions
diff --git a/gn3/auth/authorisation/checks.py b/gn3/auth/authorisation/checks.py
index 0825c84..1c87c02 100644
--- a/gn3/auth/authorisation/checks.py
+++ b/gn3/auth/authorisation/checks.py
@@ -2,12 +2,12 @@
 from functools import wraps
 from typing import Callable
 
-from flask import current_app as app
+from flask import request, current_app as app
 
 from gn3.auth import db
 
 from . import privileges as auth_privs
-from .errors import AuthorisationError
+from .errors import InvalidData, AuthorisationError
 
 from ..authentication.oauth2.resource_server import require_oauth
 
@@ -59,3 +59,12 @@ def authorised_p(
                 raise AuthorisationError(error_description)
         return __authoriser__
     return __build_authoriser__
+
+def require_json(func):
+    """Ensure the request has JSON data."""
+    @wraps(func)
+    def __req_json__(*args, **kwargs):
+        if bool(request.json):
+            return func(*args, **kwargs)
+        raise InvalidData("Expected JSON data in the request.")
+    return __req_json__
diff --git a/gn3/auth/authorisation/users/collections/__init__.py b/gn3/auth/authorisation/users/collections/__init__.py
new file mode 100644
index 0000000..88ab040
--- /dev/null
+++ b/gn3/auth/authorisation/users/collections/__init__.py
@@ -0,0 +1 @@
+"""Package dealing with user collections."""
diff --git a/gn3/auth/authorisation/users/collections.py b/gn3/auth/authorisation/users/collections/models.py
index 2e2672c..46dfb53 100644
--- a/gn3/auth/authorisation/users/collections.py
+++ b/gn3/auth/authorisation/users/collections/models.py
@@ -1,11 +1,21 @@
 """Handle user collections."""
-import uuid
 import json
+from uuid import UUID, uuid4
+from datetime import datetime
 
 from redis import Redis
 from email_validator import validate_email, EmailNotValidError
 
-from .models import User
+from ..models import User
+
+class CollectionJSONEncoder(json.JSONEncoder):
+    """Serialise collection objects into JSON."""
+    def default(self, obj):# pylint: disable=[arguments-renamed]
+        if isinstance(obj, UUID):
+            return str(obj)
+        if isinstance(obj, datetime):
+            return obj.strftime("%b %d %Y %I:%M%p")
+        return json.JSONEncoder.default(self, obj)
 
 def __valid_email__(email:str) -> bool:
     """Check for email validity."""
@@ -55,13 +65,29 @@ def __retrieve_old_accounts__(rconn: Redis) -> dict:
         }
     return __build_email_uuid_bridge__(rconn)
 
-def __retrieve_old_user_collections__(rconn: Redis, old_user_id: uuid.UUID) -> tuple:
+def parse_collection(coll: dict) -> dict:
+    """Parse the collection as persisted in redis to a usable python object."""
+    return {
+        "id": UUID(coll["id"]),
+        "name": coll["name"],
+        "created": datetime.strptime(coll["created"], "%b %d %Y %I:%M%p"),
+        "changed": datetime.strptime(coll["changed"], "%b %d %Y %I:%M%p"),
+        "num_members": int(coll["num_members"]),
+        "members": coll["members"]
+    }
+
+def dump_collection(pythoncollection: dict) -> str:
+    """Convert the collection from a python object to a json string."""
+    return json.dumps(pythoncollection, cls=CollectionJSONEncoder)
+
+def __retrieve_old_user_collections__(rconn: Redis, old_user_id: UUID) -> tuple:
     """Retrieve any old collections relating to the user."""
-    return tuple(json.loads(rconn.hget("collections", old_user_id) or "[]"))
+    return tuple(parse_collection(coll) for coll in
+                 json.loads(rconn.hget("collections", old_user_id) or "[]"))
 
-def user_collections(rconn: Redis, user: User) -> tuple:
+def user_collections(rconn: Redis, user: User) -> tuple[dict, ...]:
     """Retrieve current user collections."""
-    collections = tuple(json.loads(
+    collections = tuple(parse_collection(coll) for coll in json.loads(
         rconn.hget("collections", str(user.user_id)) or
         "[]"))
     old_accounts = __retrieve_old_accounts__(rconn)
@@ -69,9 +95,35 @@ def user_collections(rconn: Redis, user: User) -> tuple:
         not old_accounts[user.email]["collections-migrated"]):
         old_user_id = old_accounts[user.email]["user_id"]
         collections = tuple(set(collections + __retrieve_old_user_collections__(
-            rconn, uuid.UUID(old_user_id))))
+            rconn, UUID(old_user_id))))
         rconn.hdel("collections", old_user_id)
         __toggle_boolean_field__(rconn, user.email, "collections-migrated")
         rconn.hset(
             "collections", key=user.user_id, value=json.dumps(collections))
     return collections
+
+def save_collections(rconn: Redis, user: User, collections: tuple[dict, ...]) -> tuple[dict, ...]:
+    """Save the `collections` to redis."""
+    rconn.hset(
+        "collections",
+        str(user.user_id),
+        json.dumps(collections, cls=CollectionJSONEncoder))
+    return collections
+
+def add_to_user_collections(rconn: Redis, user: User, collection: dict) -> dict:
+    """Add `collection` to list of user collections."""
+    ucolls = user_collections(rconn, user)
+    save_collections(rconn, user, ucolls + (collection,))
+    return collection
+
+def create_collection(rconn: Redis, user: User, name: str, traits: tuple) -> dict:
+    """Create a new collection."""
+    now = datetime.utcnow()
+    return add_to_user_collections(rconn, user, {
+        "id": uuid4(),
+        "name": name,
+        "created": now,
+        "changed": now,
+        "num_members": len(traits),
+        "members": traits
+    })
diff --git a/gn3/auth/authorisation/users/collections/views.py b/gn3/auth/authorisation/users/collections/views.py
new file mode 100644
index 0000000..cd9458e
--- /dev/null
+++ b/gn3/auth/authorisation/users/collections/views.py
@@ -0,0 +1,73 @@
+"""Views regarding user collections."""
+from uuid import UUID
+
+from redis import Redis
+from flask import jsonify, request, Response, Blueprint, current_app
+
+from gn3.auth import db
+from gn3.auth.db_utils import with_db_connection
+from gn3.auth.authorisation.checks import require_json
+from gn3.auth.authorisation.errors import NotFoundError
+
+from gn3.auth.authentication.users import User, user_by_id
+from gn3.auth.authentication.oauth2.resource_server import require_oauth
+
+from .models import user_collections, create_collection
+
+collections = Blueprint("collections", __name__)
+
+@collections.route("/list")
+@require_oauth("profile user")
+def list_user_collections() -> Response:
+    """Retrieve the user ids"""
+    with (require_oauth.acquire("profile user") as the_token,
+          Redis.from_url(current_app.config["REDIS_URI"],
+                         decode_responses=True) as redisconn):
+        return jsonify(user_collections(redisconn, the_token.user))
+
+@collections.route("/<uuid:anon_id>/list")
+def list_anonymous_collections(anon_id: UUID) -> Response:
+    """Fetch anonymous collections"""
+    with Redis.from_url(
+            current_app.config["REDIS_URI"], decode_responses=True) as redisconn:
+        def __list__(conn: db.DbConnection) -> tuple:
+            try:
+                _user = user_by_id(conn, anon_id)
+                current_app.logger.warning(
+                    "Fetch collections for authenticated user using the "
+                    "`list_user_collections()` endpoint.")
+                return tuple()
+            except NotFoundError as _nfe:
+                return user_collections(
+                    redisconn, User(anon_id, "anon@ymous.user", "Anonymous User"))
+
+        return jsonify(with_db_connection(__list__))
+
+@require_oauth("profile user")
+def __new_collection_as_authenticated_user__(redisconn, name, traits):
+    """Create a new collection as an authenticated user."""
+    with require_oauth.acquire("profile user") as token:
+        return create_collection(redisconn, token.user, name, traits)
+
+def __new_collection_as_anonymous_user__(redisconn, name, traits):
+    """Create a new collection as an anonymous user."""
+    return create_collection(redisconn,
+                             User(UUID(request.json.get("anon_id")),
+                                  "anon@ymous.user",
+                                  "Anonymous User"),
+                             name,
+                             traits)
+
+@collections.route("/new", methods=["POST"])
+@require_json
+def new_user_collection() -> Response:
+    """Create a new collection."""
+    with (Redis.from_url(current_app.config["REDIS_URI"],
+                         decode_responses=True) as redisconn):
+        traits = tuple(request.json.get("traits", tuple()))# type: ignore[union-attr]
+        name = request.json.get("name")# type: ignore[union-attr]
+        if bool(request.headers.get("Authorization")):
+            return jsonify(__new_collection_as_authenticated_user__(
+                redisconn, name, traits))
+        return jsonify(__new_collection_as_anonymous_user__(
+            redisconn, name, traits))
diff --git a/gn3/auth/authorisation/users/views.py b/gn3/auth/authorisation/users/views.py
index a0f00de..ae39110 100644
--- a/gn3/auth/authorisation/users/views.py
+++ b/gn3/auth/authorisation/users/views.py
@@ -1,11 +1,9 @@
 """User authorisation endpoints."""
 import traceback
-from uuid import UUID
 from typing import Any
 from functools import partial
 
 import sqlite3
-from redis import Redis
 from email_validator import validate_email, EmailNotValidError
 from flask import request, jsonify, Response, Blueprint, current_app
 
@@ -14,7 +12,7 @@ from gn3.auth.dictify import dictify
 from gn3.auth.db_utils import with_db_connection
 
 from .models import list_users
-from .collections import user_collections
+from .collections.views import collections
 
 from ..groups.models import user_group as _user_group
 from ..resources.models import user_resources as _user_resources
@@ -23,11 +21,11 @@ from ..errors import (
     NotFoundError, UsernameError, PasswordError, UserRegistrationError)
 
 from ...authentication.oauth2.resource_server import require_oauth
+from ...authentication.users import User, save_user, set_user_password
 from ...authentication.oauth2.models.oauth2token import token_by_access_token
-from ...authentication.users import (
-    User, save_user, user_by_id, set_user_password)
 
 users = Blueprint("users", __name__)
+users.register_blueprint(collections, url_prefix="/collections")
 
 @users.route("/", methods=["GET"])
 @require_oauth("profile")
@@ -174,30 +172,3 @@ def list_all_users() -> Response:
     with require_oauth.acquire("profile group") as _the_token:
         return jsonify(tuple(
             dictify(user) for user in with_db_connection(list_users)))
-
-@users.route("collections/list")
-@require_oauth("profile user")
-def list_user_collections() -> Response:
-    """Retrieve the user ids"""
-    with (require_oauth.acquire("profile user") as the_token,
-          Redis.from_url(current_app.config["REDIS_URI"],
-                         decode_responses=True) as redisconn):
-        return jsonify(user_collections(redisconn, the_token.user))
-
-@users.route("<uuid:anon_id>/collections/list")
-def list_anonymous_collections(anon_id: UUID) -> Response:
-    """Fetch anonymous collections"""
-    with Redis.from_url(
-            current_app.config["REDIS_URI"], decode_responses=True) as redisconn:
-        def __list__(conn: db.DbConnection) -> tuple:
-            try:
-                _user = user_by_id(conn, anon_id)
-                current_app.logger.warning(
-                    "Fetch collections for authenticated user using the "
-                    "`list_user_collections()` endpoint.")
-                return tuple()
-            except NotFoundError as _nfe:
-                return user_collections(
-                    redisconn, User(anon_id, "anon@ymous.user", "Anonymous User"))
-
-        return jsonify(with_db_connection(__list__))