From 062daa0f056e857f04c648c002faf6dcebfb30f0 Mon Sep 17 00:00:00 2001 From: npalaska Date: Thu, 11 Mar 2021 00:23:02 -0500 Subject: [PATCH] Add a key and client defined value column to metadata table --- lib/pbench/server/api/__init__.py | 4 +- .../server/api/resources/metadata_api.py | 146 ++++++++++-------- lib/pbench/server/database/models/metadata.py | 43 +++--- .../unit/server/test_metadata_sessions.py | 102 ++++++++---- 4 files changed, 184 insertions(+), 111 deletions(-) diff --git a/lib/pbench/server/api/__init__.py b/lib/pbench/server/api/__init__.py index c96e6eace1..2b544de3ca 100644 --- a/lib/pbench/server/api/__init__.py +++ b/lib/pbench/server/api/__init__.py @@ -88,12 +88,12 @@ def register_endpoints(api, app, config): api.add_resource( UserMetadata, - f"{base_uri}/metadata", + f"{base_uri}/metadata/", resource_class_args=(config, logger, token_auth), ) api.add_resource( QueryMetadata, - f"{base_uri}/metadata/", + f"{base_uri}/metadata/", resource_class_args=(config, logger), ) diff --git a/lib/pbench/server/api/resources/metadata_api.py b/lib/pbench/server/api/resources/metadata_api.py index 8305cdeeb5..71a5f2fb5e 100644 --- a/lib/pbench/server/api/resources/metadata_api.py +++ b/lib/pbench/server/api/resources/metadata_api.py @@ -1,6 +1,6 @@ from flask_restful import Resource, abort from flask import request, make_response, jsonify -from pbench.server.database.models.metadata import Metadata +from pbench.server.database.models.metadata import Metadata, MetadataKeys from pbench.server.api.auth import Auth @@ -14,58 +14,70 @@ def __init__(self, config, logger, auth): self.logger = logger self.auth = auth - @Auth.token_auth.login_required() - def post(self): + @Auth.token_auth.login_required(optional=True) + def post(self, key): """ Post request for creating metadata instance for a user. - This requires a Pbench auth token in the header field + + The url requires a metadata session key such as /metadata/ + the only keys acceptable to create the metadata sessions are favourite/saved This requires a JSON data with required user metadata fields { - "config": "config", - "description": "description", + "value": "blog text" , } + Example: {"value": '{"config": "config", "description": "description"}'} - Required headers include + Authorization header can be included as Authorization: Bearer + If the authorization header is not present, the created metadata session becomes public by default :return: JSON Payload response_object = { "message": "success" "data" { "id": "metadata_id", - "config": "Config string" - "description": "Description string" + "value": "client text blob", + "created": "datetime string", + "updated": "datetime string", + "key": favorite/saved } } """ + metadata_key = key.upper() + if metadata_key not in [key.name for key in MetadataKeys]: + self.logger.warning( + "Invalid Metadata key provided during metadata creation. Key: {}", + metadata_key, + ) + abort( + 400, + message="Invalid metadata key. Key need to be either Favorite/Saved", + ) + post_data = request.get_json() if not post_data: self.logger.warning("Invalid json object: {}", request.url) abort(400, message="Invalid json object in request") - current_user_id = self.auth.token_auth.current_user().id - - config = post_data.get("config") - if not config: - self.logger.warning( - "Config not provided during metadata creation. user_id: {}", - current_user_id, - ) - abort(400, message="Config field missing") + current_user = self.auth.token_auth.current_user() + if current_user: + current_user_id = current_user.id + else: + current_user_id = None - description = post_data.get("description") - if not description: + value = post_data.get("value") + if not value: self.logger.warning( - "Description not provided during metadata creation by user: {}", + "value not provided during metadata creation. user_id: {}", current_user_id, ) - abort(400, message="Description field missing") + abort(400, message="Value field missing") try: # Create a new metadata session metadata_session = Metadata( - config=config, description=description, user_id=current_user_id + value=value, key=metadata_key, user_id=current_user_id ) # insert the metadata session for a user metadata_session.add() @@ -78,16 +90,12 @@ def post(self): else: response_object = { "message": "success", - "data": { - "id": metadata_session.id, - "config": metadata_session.config, - "description": metadata_session.description, - }, + "data": metadata_session.get_json(), } return make_response(jsonify(response_object), 201) - @Auth.token_auth.login_required() - def get(self): + @Auth.token_auth.login_required(optional=True) + def get(self, key): """ Get request for querying all the metadata sessions for a user. returns the list of all the metadata sessions associated with a logged in user. @@ -98,20 +106,38 @@ def get(self): :return: JSON Payload response_object = { - "status": "success" + "message": "success" "data": { "sessions": [ {"id": "metadata_id", - "config": "Config string" - "description": "Description string"}, + "value": "client text blob", + "key": "key string", + "created": "datetime string", + "updated": "datetime string", }, {}] } } """ - current_user_id = self.auth.token_auth.current_user().id + if not key: + self.logger.warning("Metadata key not provided during metadata query") + abort(400, message="Missing metadata key in the query url") + + current_user = self.auth.token_auth.current_user() + if current_user: + current_user_id = current_user.id + else: + current_user_id = None + print(current_user_id) try: - # Fetch the metadata session - metadata_sessions = Metadata.query(user_id=current_user_id) + # Fetch the metadata sessions + # If the key is favorite, we return only favorite sessions, + # else we return all the saved and favorite sessions + if key.upper() == "FAVORITE": + metadata_sessions = Metadata.query( + user_id=current_user_id, key=MetadataKeys[key.upper()].value + ) + else: + metadata_sessions = Metadata.query(user_id=current_user_id) data = [session.get_json() for session in metadata_sessions] except Exception: self.logger.exception( @@ -153,18 +179,20 @@ def get(self, id=None): Get request for querying a metadata session of a user given a metadata id. This requires a Pbench auth token in the header field - The url requires a metadata session id such as /user/metadata/ + The url requires a metadata session id such as /user/metadata/ Required headers include Authorization: Bearer :return: JSON Payload response_object = { - "message": "success" - "data" { + "message": "success", + "data": { "id": "metadata_id", - "config": "Config string" - "description": "Description string" + "value": "client text blob" + "created": "Datetime string" + "updated": "Datetime String" + "key": "key string" } } """ @@ -174,7 +202,7 @@ def get(self, id=None): try: # Fetch the metadata session - metadata_session = Metadata.query(id=id) + metadata_session = Metadata.query(id=id)[0] except Exception: self.logger.exception( "Exception occurred in the GET request while querying the Metadata model, id: {}", @@ -187,11 +215,7 @@ def get(self, id=None): response_object = { "message": "success", - "data": { - "id": metadata_session.id, - "config": metadata_session.config, - "description": metadata_session.description, - }, + "data": metadata_session.get_json(), } return make_response(jsonify(response_object), 200) @@ -201,23 +225,27 @@ def put(self, id=None): Put request for updating a metadata session of a user given a metadata id. This requires a Pbench auth token in the header field - The url requires a metadata session id such as /user/metadata/ + The url requires a metadata session id such as /user/metadata/ This requires a JSON data with required user metadata fields to update { - "description": "description", + "value": "new text value", + ... } + To update the value field, it is required to send the complete text blob again Required headers include Authorization: Bearer :return: JSON Payload response_object = { - "message": "success" - "data" { + "message": "success", + "data": { "id": "metadata_id", - "config": "Config string" - "description": "Description string" + "value": "client text blob" + "created": "Datetime string" + "updated": "Datetime String" + "key": "key string" } } """ @@ -231,7 +259,7 @@ def put(self, id=None): abort(400, message="Invalid json object in request") try: - metadata_session = Metadata.query(id=id) + metadata_session = Metadata.query(id=id)[0] except Exception: self.logger.exception( "Exception occurred in the PUT request while querying the Metadata model, id: {}", @@ -274,11 +302,7 @@ def put(self, id=None): else: response_object = { "message": "success", - "data": { - "id": metadata_session.id, - "config": metadata_session.config, - "description": metadata_session.description, - }, + "data": metadata_session.get_json(), } return make_response(jsonify(response_object), 200) @@ -288,7 +312,7 @@ def delete(self, id=None): Delete request for deleting a metadata session of a user given a metadata id. This requires a Pbench auth token in the header field - The url requires a metadata session id such as /user/metadata/ + The url requires a metadata session id such as /user/metadata/ Required headers include Authorization: Bearer @@ -304,7 +328,7 @@ def delete(self, id=None): try: # Fetch the metadata session - metadata_session = Metadata.query(id=id) + metadata_session = Metadata.query(id=id)[0] except Exception: self.logger.exception( "Exception occurred in the Delete request while querying the Metadata model, id: {}", diff --git a/lib/pbench/server/database/models/metadata.py b/lib/pbench/server/database/models/metadata.py index c835fd7945..cd25d8f149 100644 --- a/lib/pbench/server/database/models/metadata.py +++ b/lib/pbench/server/database/models/metadata.py @@ -1,6 +1,13 @@ +import enum import datetime from pbench.server.database.database import Database -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text +from sqlalchemy import Column, Integer, DateTime, ForeignKey, Text +from sqlalchemy.orm import validates + + +class MetadataKeys(enum.Enum): + FAVORITE = 1 + SAVED = 2 class Metadata(Database.Base): @@ -12,38 +19,34 @@ class Metadata(Database.Base): id = Column(Integer, primary_key=True, autoincrement=True) created = Column(DateTime, nullable=False, default=datetime.datetime.now()) updated = Column(DateTime, nullable=False, default=datetime.datetime.now()) - config = Column(Text, unique=False, nullable=False) - description = Column(String(255), nullable=False) - user_id = Column(Integer, ForeignKey("users.id")) + value = Column(Text, unique=False, nullable=False) + key = Column(Integer, nullable=False) + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True) - def __str__(self): - return f"Url id: {self.id}, created on: {self.created}, description: {self.description}" + @validates("key") + def evaluate_key(self, key, value): + return MetadataKeys[value].value def get_json(self): return { "id": self.id, - "config": self.config, - "description": self.description, + "value": self.value, "created": self.created, "updated": self.updated, + "key": MetadataKeys(self.key).name, } @staticmethod def get_protected(): - return ["id", "created"] + return ["id", "created", "user_id"] @staticmethod - def query(id=None, user_id=None): - # Currently we would only query with single argument. Argument need to be either id/user_id - if id: - metadata = Database.db_session.query(Metadata).filter_by(id=id).first() - elif user_id: - # If the query parameter is user_id then we return the list of all the metadata linked to that user - metadata = Database.db_session.query(Metadata).filter_by(user_id=user_id) - else: - metadata = None - - return metadata + def query(**kwargs): + query = Database.db_session.query(Metadata) + for attr, value in kwargs.items(): + print(getattr(Metadata, attr), value) + query = query.filter(getattr(Metadata, attr) == value) + return query.all() def add(self): """ diff --git a/lib/pbench/test/unit/server/test_metadata_sessions.py b/lib/pbench/test/unit/server/test_metadata_sessions.py index d408086fd7..5d961c89b1 100644 --- a/lib/pbench/test/unit/server/test_metadata_sessions.py +++ b/lib/pbench/test/unit/server/test_metadata_sessions.py @@ -24,55 +24,98 @@ def user_register_login(client, server_config): class TestMetadataSession: @staticmethod - def test_metadata_creation(client, server_config): + def test_metadata_creation_with_authorization(client, server_config): data_login = user_register_login(client, server_config) with client: + # create a favorite session response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config1", "description": "description1"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json assert data["message"] == "success" + # create a saved session response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config2", "description": "description2"}, + f"{server_config.rest_uri}/metadata/saved", + json={"value": '{"config": "config2", "description": "description2"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json assert data["message"] == "success" + # Get all the favorite sessions of logged in user response = client.get( - f"{server_config.rest_uri}/metadata", + f"{server_config.rest_uri}/metadata/favorite", headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) + assert response.status_code == 200 data = response.json assert data["message"] == "success" - assert data["data"]["sessions"] + assert ( + data["data"]["sessions"][0]["value"] + == '{"config": "config1", "description": "description1"}' + ) + + # Get all the saved sessions of logged in user + response = client.get( + f"{server_config.rest_uri}/metadata/saved", + headers=dict(Authorization="Bearer " + data_login["auth_token"]), + ) assert response.status_code == 200 + data = response.json + assert data["message"] == "success" + assert len(data["data"]["sessions"]) == 2 @staticmethod def test_unauthorized_metadata_creation(client, server_config): with client: + # Create a saved session response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/saved", + json={"value": '{"config": "config1", "description": "description1"}'}, ) data = response.json - assert data is None - assert response.status_code == 401 + assert data + assert response.status_code == 201 + + # Create a favorite session + response = client.post( + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config2", "description": "description2"}'}, + ) + data = response.json + assert data["message"] == "success" + + # Get all the favorite sessions of non-logged in user + response = client.get(f"{server_config.rest_uri}/metadata/favorite") + assert response.status_code == 200 + data = response.json + assert data["message"] == "success" + assert ( + data["data"]["sessions"][0]["value"] + == '{"config": "config2", "description": "description2"}' + ) + + # Get all the saved sessions of non-logged in user + response = client.get(f"{server_config.rest_uri}/metadata/saved",) + assert response.status_code == 200 + data = response.json + assert data["message"] == "success" + assert len(data["data"]["sessions"]) == 2 @staticmethod def test_single_metadata_query(client, server_config): data_login = user_register_login(client, server_config) with client: response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config1", "description": "description1"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json + assert response.status_code == 201 assert data["message"] == "success" assert data["data"]["id"] @@ -83,7 +126,7 @@ def test_single_metadata_query(client, server_config): ) data = response.json assert data["message"] == "success" - assert data["data"]["config"] == "config1" + assert data["data"]["key"] == "FAVORITE" @staticmethod def test_unauthorized_metadata_query1(client, server_config): @@ -91,8 +134,8 @@ def test_unauthorized_metadata_query1(client, server_config): data_login = user_register_login(client, server_config) with client: response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config1", "description": "description1"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json @@ -109,8 +152,8 @@ def test_unauthorized_metadata_query2(client, server_config): data_login_1 = user_register_login(client, server_config) with client: response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config1", "description": "description1"}'}, headers=dict(Authorization="Bearer " + data_login_1["auth_token"]), ) data_1 = response.json @@ -136,8 +179,8 @@ def test_unauthorized_metadata_query2(client, server_config): # Create metadata session for 2nd user response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config2", "description": "description2"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config2", "description": "description2"}'}, headers=dict(Authorization="Bearer " + data_login_2["auth_token"]), ) data_2 = response.json @@ -159,8 +202,8 @@ def test_metadata_update(client, server_config): data_login = user_register_login(client, server_config) with client: response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config1", "description": "description1"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json @@ -170,20 +213,23 @@ def test_metadata_update(client, server_config): metadata_id = data["data"]["id"] response = client.put( f"{server_config.rest_uri}/metadata/{metadata_id}", - json={"description": "description2"}, + json={"value": '{"config": "config1", "description": "description2"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json assert data["message"] == "success" - assert data["data"]["description"] == "description2" + assert ( + data["data"]["value"] + == '{"config": "config1", "description": "description2"}' + ) @staticmethod def test_metadata_update_with_invalid_fields(client, server_config): data_login = user_register_login(client, server_config) with client: response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config1", "description": "description2"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json @@ -205,8 +251,8 @@ def test_metadata_delete(client, server_config): data_login = user_register_login(client, server_config) with client: response = client.post( - f"{server_config.rest_uri}/metadata", - json={"config": "config1", "description": "description1"}, + f"{server_config.rest_uri}/metadata/favorite", + json={"value": '{"config": "config1", "description": "description2"}'}, headers=dict(Authorization="Bearer " + data_login["auth_token"]), ) data = response.json