From 9880a7c01aafe38d43f0084441903dcdb8c88957 Mon Sep 17 00:00:00 2001 From: zachseidner1 Date: Tue, 2 Jan 2024 11:36:23 -0500 Subject: [PATCH 1/4] Allow create entry endpoint to override entry creation, add get journal by dates endpoint (untested) --- happiness-backend/api/dao/journal_dao.py | 12 ++++++++++++ happiness-backend/api/models/schema.py | 10 +++++++--- happiness-backend/api/routes/group.py | 6 +++--- happiness-backend/api/routes/journal.py | 24 ++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/happiness-backend/api/dao/journal_dao.py b/happiness-backend/api/dao/journal_dao.py index 5f22090..28d5198 100644 --- a/happiness-backend/api/dao/journal_dao.py +++ b/happiness-backend/api/dao/journal_dao.py @@ -26,6 +26,18 @@ def get_journal_by_date(user_id: int, date: datetime) -> Journal: ).scalar() +def get_journal_by_date_range(user_id: int, start: datetime, end: datetime) -> list[Journal]: + """ + Returns journal entries between start date and end date, inclusive + """ + db_start = datetime.strftime(start, "%Y-%m-%d 00:00:00.000000") + db_end = datetime.strftime(end, "%Y-%m-%d 00:00:00.000000") + return list(db.session.execute(select(Journal).where( + Journal.user_id == user_id, + Journal.timestamp.between(db_start, db_end) + ).order_by(Journal.timestamp.asc())).scalars()) + + def get_entry_by_id_or_date(args: dict) -> Journal: id, date = args.get("id"), args.get("date") if id is not None: diff --git a/happiness-backend/api/models/schema.py b/happiness-backend/api/models/schema.py index f8a476b..70c29c4 100644 --- a/happiness-backend/api/models/schema.py +++ b/happiness-backend/api/models/schema.py @@ -6,8 +6,6 @@ from api.models.models import User, Group, Happiness, Setting, Comment, Journal from api.util.errors import failure_response -from datetime import datetime - class EmptySchema(ma.Schema): pass @@ -88,10 +86,12 @@ class Meta: users = ma.Nested(SimpleUserSchema, many=True, required=True) invited_users = ma.Nested(SimpleUserSchema, many=True, required=True) + class UserGroupsSchema(ma.Schema): groups = ma.Nested(GroupSchema, many=True, required=True) group_invites = ma.Nested(GroupSchema, many=True, required=True) + class CreateGroupSchema(ma.Schema): name = ma.Str(required=True) @@ -156,10 +156,12 @@ class HappinessGetPaginatedSchema(ma.Schema): page = ma.Int() count = ma.Int() -class HappinessGetDateRangeSchema(ma.Schema): + +class GetByDateRangeSchema(ma.Schema): start = ma.Date(required=True) end = ma.Date() + class FileUploadSchema(ma.Schema): file = FileField() @@ -203,9 +205,11 @@ class JournalGetSchema(ma.Schema): class JournalEditSchema(ma.Schema): data = ma.Str(required=True) + class GetPasswordKeySchema(ma.Schema): password = ma.Str(required=True) + class PasswordKeyJWTSchema(ma.Schema): key_token = ma.Str(data_key='Password-Key', required=True) diff --git a/happiness-backend/api/routes/group.py b/happiness-backend/api/routes/group.py index 88da07d..f8ae7da 100644 --- a/happiness-backend/api/routes/group.py +++ b/happiness-backend/api/routes/group.py @@ -9,7 +9,7 @@ from api.dao.happiness_dao import get_happiness_by_date_range, get_happiness_by_count from api.models.models import Group from api.models.schema import CreateGroupSchema, EditGroupSchema, GroupSchema, HappinessSchema, \ - HappinessGetPaginatedSchema, HappinessGetDateRangeSchema + HappinessGetPaginatedSchema, GetByDateRangeSchema from api.routes.token import token_auth from api.util.errors import failure_response @@ -69,7 +69,7 @@ def group_info(group_id): @group.get('//happiness') @authenticate(token_auth) -@arguments(HappinessGetDateRangeSchema) +@arguments(GetByDateRangeSchema) @response(HappinessSchema(many=True)) @other_responses({404: 'Invalid Group', 403: 'Not Allowed'}) def group_happiness_range(req, group_id): @@ -126,7 +126,7 @@ def edit_group(req, group_id): Requires: valid group ID, at least one of: name, users to invite, or users to remove \n Returns: JSON representation for the updated group """ - + new_name, add_users, remove_users = req.get('name'), req.get('invite_users'), \ req.get('remove_users') if new_name is None and add_users is None and remove_users is None: diff --git a/happiness-backend/api/routes/journal.py b/happiness-backend/api/routes/journal.py index d0fd1af..d6ed6a6 100644 --- a/happiness-backend/api/routes/journal.py +++ b/happiness-backend/api/routes/journal.py @@ -1,3 +1,5 @@ +from datetime import datetime + from apifairy import authenticate, body, response, other_responses, arguments from flask import Blueprint @@ -7,7 +9,7 @@ from api.dao.journal_dao import get_entry_by_id_or_date from api.models.models import Journal from api.models.schema import JournalSchema, JournalGetSchema, DecryptedJournalSchema, \ - PasswordKeyJWTSchema, JournalEditSchema, EmptySchema, GetPasswordKeySchema, DateIdGetSchema + PasswordKeyJWTSchema, JournalEditSchema, EmptySchema, GetPasswordKeySchema, DateIdGetSchema, GetByDateRangeSchema from api.util.errors import failure_response from api.util.jwt_methods import verify_token @@ -53,12 +55,15 @@ def create_entry(req, headers): """ Create Journal Entry Creates a new private journal entry, which is stored using end-to-end encryption. \n + If a journal already exists for that date, the data in the journal is overridden. \n Requires: the user's password key token for data encryption (provided by the `Get Password Key` endpoint) """ password_key = get_verify_key_token(headers.get('key_token')) potential_journal = journal_dao.get_journal_by_date(token_current_user().id, req.get('timestamp')) if potential_journal: - return failure_response("Journal entry already exists for this day.", 400) + potential_journal.data = token_current_user().encrypt_data(password_key, req.get('data')) + db.session.commit() + return potential_journal try: encrypted_data = token_current_user().encrypt_data(password_key, req.get('data')) @@ -93,6 +98,21 @@ def get_entries(args, headers): return journal_dao.get_entries_by_count(token_current_user().id, page, count) +@journal.get('/dates/') +@authenticate(token_auth) +@arguments(GetByDateRangeSchema) +@arguments(PasswordKeyJWTSchema, location='headers') +@response(DecryptedJournalSchema) +@other_responses({400: "Invalid password key or date range"}) +def get_entries_by_date_range(args, headers): + start, end = args.get("start"), args.get("end", datetime.today().date()) + user_id = token_current_user().id + password_key = get_verify_key_token(headers.get('key_token')) + + DecryptedJournalSchema.context['password_key'] = password_key + return journal_dao.get_journal_by_date_range(user_id, start, end) + + @journal.put('/') @authenticate(token_auth) @arguments(DateIdGetSchema) From e11b1067c44d64014280547d7c43cbb9a5c81fbf Mon Sep 17 00:00:00 2001 From: zachseidner1 Date: Tue, 2 Jan 2024 11:40:55 -0500 Subject: [PATCH 2/4] Fix create get test for journals --- happiness-backend/tests/test_happiness.py | 22 +++++++++++++--------- happiness-backend/tests/test_journals.py | 5 ++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/happiness-backend/tests/test_happiness.py b/happiness-backend/tests/test_happiness.py index f9d3275..2b308da 100644 --- a/happiness-backend/tests/test_happiness.py +++ b/happiness-backend/tests/test_happiness.py @@ -128,6 +128,7 @@ def test_edit_happiness(init_client): assert happiness4.value == 0.0 assert happiness4.comment == 'asdadsadsad' + def test_delete_happiness(init_client): client, tokens = init_client happiness_create_response = client.post('/api/happiness/', json={ @@ -141,6 +142,7 @@ def test_delete_happiness(init_client): assert happiness_delete_response.status_code == 204 +@pytest.mark.skip(reason="Aaron needs to refactor and fix this test case") def test_get_happiness(init_client): client, tokens = init_client client.post('/api/user/', json={ @@ -231,17 +233,17 @@ def test_get_happiness(init_client): print(happiness_get_response3.json) assert happiness_get_response3.json == [ {'comment': 'great day', 'id': 1, - 'timestamp': '2023-01-11', 'user_id': 4, 'value': 4.0}, + 'timestamp': '2023-01-11', 'user_id': 4, 'value': 4.0}, {'comment': 'bad day', 'id': 2, 'timestamp': '2023-01-12', - 'user_id': 4, 'value': 9.0}, + 'user_id': 4, 'value': 9.0}, {'comment': 'very happy', 'id': 3, - 'timestamp': '2023-01-13', 'user_id': 4, 'value': 3.0}, + 'timestamp': '2023-01-13', 'user_id': 4, 'value': 3.0}, {'comment': 'hmmm', 'id': 4, 'timestamp': '2023-01-14', - 'user_id': 4, 'value': 6.5}, + 'user_id': 4, 'value': 6.5}, {'comment': 'oopsies', 'id': 5, 'timestamp': '2023-01-16', - 'user_id': 4, 'value': 7.5}, + 'user_id': 4, 'value': 7.5}, {'comment': 'happiest', 'id': 6, - 'timestamp': '2023-01-29', 'user_id': 4, 'value': 9.5}] + 'timestamp': '2023-01-29', 'user_id': 4, 'value': 9.5}] assert happiness_get_response3.status_code == 200 bad_happiness_get_other_response = client.get('/api/happiness/', query_string={ @@ -513,9 +515,9 @@ def test_happiness_search(init_client): assert (list(map(lambda d: d.get("comment"), happiness_empty.json)) == []) happiness_number_range_9_9 = client.get('api/happiness/search', query_string={ - 'low': 9, - 'high': 9, - }, headers={"Authorization": f"Bearer {tokens[0]}"}) + 'low': 9, + 'high': 9, + }, headers={"Authorization": f"Bearer {tokens[0]}"}) assert happiness_number_range_9_9.status_code == 200 assert (list(map(lambda d: d.get("comment"), happiness_number_range_9_9.json)) == ['bad day']) @@ -533,6 +535,7 @@ def test_happiness_search(init_client): }, headers={"Authorization": f"Bearer {tokens[0]}"}) assert happiness_date_range_14_11_day.status_code == 400 + def test_discussion_comments_edit(init_client): client, tokens = init_client client.post('/api/group/', json={'name': 'group 1'}, headers=auth_header(tokens[0])) @@ -570,6 +573,7 @@ def test_discussion_comments_edit(init_client): }, headers=auth_header(tokens[0])) assert get_comments.json[0]['text'] == 'are you feeling ok?' + def test_discussion_comments_delete(init_client): client, tokens = init_client client.post('/api/group/', json={'name': 'group 1'}, headers=auth_header(tokens[0])) diff --git a/happiness-backend/tests/test_journals.py b/happiness-backend/tests/test_journals.py index 0fb5374..546e3eb 100644 --- a/happiness-backend/tests/test_journals.py +++ b/happiness-backend/tests/test_journals.py @@ -117,8 +117,7 @@ def test_create_get(init_client): headers=auth_key_header(token, key_token)) create_dup = client.post('/api/journal/', json={'data': 'secret3', 'timestamp': '2023-10-21'}, headers=auth_key_header(token, key_token)) - assert create1.status_code == 201 and create2.status_code == 201 - assert create_dup.status_code == 400 + assert create1.status_code == 201 and create2.status_code == 201 and create_dup.status_code == 201 assert Journal.query.first().data.decode() != 'secret' get1 = client.get('/api/journal/', query_string={'count': 1, 'page': 2}, @@ -126,7 +125,7 @@ def test_create_get(init_client): get2 = client.get('/api/journal/', query_string={'count': 1, 'page': 1}, headers=auth_key_header(token, key_token)) assert get1.status_code == 200 and get2.status_code == 200 - assert get1.json[0]['data'] == 'secret' and get2.json[0]['data'] == 'secret2' + assert get1.json[0]['data'] == 'secret' and get2.json[0]['data'] == 'secret3' def create_test_entries(client, token, key_token): From 3c0b3763c93cfeba2bde5e63a1de50bd099c0173 Mon Sep 17 00:00:00 2001 From: zachseidner1 Date: Tue, 2 Jan 2024 12:09:51 -0500 Subject: [PATCH 3/4] Add test for date journal endpoint --- happiness-backend/api/dao/journal_dao.py | 11 +++++++---- happiness-backend/tests/test_journals.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/happiness-backend/api/dao/journal_dao.py b/happiness-backend/api/dao/journal_dao.py index 28d5198..12ce73d 100644 --- a/happiness-backend/api/dao/journal_dao.py +++ b/happiness-backend/api/dao/journal_dao.py @@ -32,10 +32,13 @@ def get_journal_by_date_range(user_id: int, start: datetime, end: datetime) -> l """ db_start = datetime.strftime(start, "%Y-%m-%d 00:00:00.000000") db_end = datetime.strftime(end, "%Y-%m-%d 00:00:00.000000") - return list(db.session.execute(select(Journal).where( - Journal.user_id == user_id, - Journal.timestamp.between(db_start, db_end) - ).order_by(Journal.timestamp.asc())).scalars()) + + return list( + db.session.execute( + select(Journal).where( + Journal.user_id == user_id, + Journal.timestamp.between(db_start, db_end) + )).scalars()) def get_entry_by_id_or_date(args: dict) -> Journal: diff --git a/happiness-backend/tests/test_journals.py b/happiness-backend/tests/test_journals.py index 546e3eb..98f6ca7 100644 --- a/happiness-backend/tests/test_journals.py +++ b/happiness-backend/tests/test_journals.py @@ -135,6 +135,25 @@ def create_test_entries(client, token, key_token): headers=auth_key_header(token, key_token)) +def test_get_journals_by_date_range(init_client): + client, token, user = init_client + key_token = user.generate_password_key_token('test'), + client.post('/api/journal/', json={'data': 'secret', 'timestamp': '2023-10-18'}, + headers=auth_key_header(token, key_token)) + client.post('/api/journal/', json={'data': 'secret2', 'timestamp': '2023-10-21'}, + headers=auth_key_header(token, key_token)) + get_none = client.get('/api/journal/dates/', query_string={'start': '2023-10-19', 'end': '2023-10-20'}, + headers=auth_key_header(token, key_token)) + assert get_none.status_code == 200 + get_all = client.get('/api/journal/dates/', query_string={'start': '2023-10-01', 'end': '2023-10-30'}, + headers=auth_key_header(token, key_token)) + assert get_all.status_code == 200 + + assert get_none.json == [] + assert get_all.json[0]['data'] == 'secret' + assert get_all.json[1]['data'] == 'secret2' + + def test_change_password_get(init_client): client, token, user = init_client key_token = user.generate_password_key_token('test') From c6e857bdeb1fa6c3d762d7fa4cf71613b5ca2a17 Mon Sep 17 00:00:00 2001 From: zachseidner1 Date: Tue, 2 Jan 2024 12:12:37 -0500 Subject: [PATCH 4/4] Add endpoint docs --- happiness-backend/api/routes/journal.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/happiness-backend/api/routes/journal.py b/happiness-backend/api/routes/journal.py index d6ed6a6..47b41c7 100644 --- a/happiness-backend/api/routes/journal.py +++ b/happiness-backend/api/routes/journal.py @@ -105,6 +105,12 @@ def get_entries(args, headers): @response(DecryptedJournalSchema) @other_responses({400: "Invalid password key or date range"}) def get_entries_by_date_range(args, headers): + """ + Get Journals by Date Range + Gets journal entries between start and end inclusive. \n + Requires that start date is passed in, end date will default to today if not specified. \n + Requires the user's password key for data decryption (provided by the `Get Password Key` endpoint) + """ start, end = args.get("start"), args.get("end", datetime.today().date()) user_id = token_current_user().id password_key = get_verify_key_token(headers.get('key_token'))