From 20fc93f6084ff1962efd89a18193a2fe4d560582 Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 28 Oct 2024 10:58:42 -0400 Subject: [PATCH 1/8] Add endpoint for removing expired lco scheduler events --- handler.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ serverless.yml | 8 +++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/handler.py b/handler.py index 8a70d70..9aed054 100644 --- a/handler.py +++ b/handler.py @@ -127,6 +127,43 @@ def getProject(project_name, created_at): return "Project not found." +def remove_expired_scheduler_events(cutoff_time, site): + table = dynamodb.Table(calendar_table_name) + index_name = "site-end-index" + + # Query items from the secondary index with 'site' as the partition key and 'end' greater than the specified end_date + # We're using 'end' time for the query because it's part of a pre-existing GSI that allows for efficient queries. + # But ultimately we want this to apply to events that start after the cutoff, so add that as a filter condition too. + query = table.query( + IndexName=index_name, + KeyConditionExpression=Key('site').eq(site) & Key('end').gt(cutoff_time), + FilterExpression=Attr('origin').eq('lco') & Attr('start').gt(cutoff_time) + ) + items = query.get('Items', []) + + # Extract key attributes for deletion (use the primary key attributes, not the index keys) + key_names = [k['AttributeName'] for k in table.key_schema] + + with table.batch_writer() as batch: + for item in items: + batch.delete_item(Key={k: item[k] for k in key_names if k in item}) + + # Handle pagination if results exceed 1MB + while 'LastEvaluatedKey' in query: + query = table.query( + IndexName=index_name, + KeyConditionExpression=Key('site').eq(site) & Key('end').gt(cutoff_time), + FilterExpression=Attr('origin').eq('lco') & Attr('start').gt(cutoff_time), + ExclusiveStartKey=query['LastEvaluatedKey'] + ) + items = query.get('Items', []) + + with table.batch_writer() as batch: + for item in items: + batch.delete_item(Key={k: item[k] for k in key_names if k in item}) + + + #=========================================# #======= API Endpoints ========# #=========================================# @@ -392,6 +429,22 @@ def deleteEventById(event, context): print(f"success deleting event, message: {message}") return create_response(200, message) +def clearExpiredSchedule(event, context): + """Endpoint to delete calendar events with an event_id. + + Args: + event.body.site (str): + sitecode for the site we are dealing with + event.body.time (str): + UTC datestring (eg. '2022-05-14T17:30:00Z'). All events that start after this will be removed. + + Returns: + 200 status code + """ + event_body = json.loads(event.get("body", "")) + remove_expired_scheduler_events(event_body["cutoff_time"], event_body["site"]) + return create_response(200) + def getSiteEventsInDateRange(event, context): """Return calendar events within a specified date range at a given site. diff --git a/serverless.yml b/serverless.yml index cc2b80e..9224ecc 100644 --- a/serverless.yml +++ b/serverless.yml @@ -265,7 +265,13 @@ functions: - X-Amz-User-Agent - Access-Control-Allow-Origin - Access-Control-Allow-Credentials - + clearExpiredSchedule: + handler: handler.clearExpiredSchedule + events: + - http: + path: remove-expired-lco-schedule + method: post + cors: true getSiteEventsInDateRange: handler: handler.getSiteEventsInDateRange events: From 4116df154b8c8eeb250ef6597ebb3a11eb417ae7 Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 28 Oct 2024 11:11:08 -0400 Subject: [PATCH 2/8] Ignore deployment if just editing the readme --- .github/workflows/deployment.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index d4c8317..8bc92e8 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -3,6 +3,8 @@ name: Deploy Script on: push: branches: [main, dev] + paths-ignore: + - 'README.md' jobs: deploy: From fbf73c2a4f10b76effef91418f1df30bbcd2cbef Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 28 Oct 2024 11:28:27 -0400 Subject: [PATCH 3/8] Update readme with new endpoint --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0133151..9dd91e3 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ The body of a calendar event follows the JSON format below: ```javascript { "event_id": "024242f...", // Unique ID generated for each new reservation - "start": "2022-06-20T06:15:00Z", // Starting UTC date and time of reservation - "end": "2022-06-20T06:45:00Z", // Ending UTC date and time of reservation + "start": "2022-06-20T16:15:00Z", // Starting UTC date and time of reservation + "end": "2022-06-20T16:45:00Z", // Ending UTC date and time of reservation "creator": "Firstname Lastname", // String of user display name "creator_id": "google-oauth2|xxxxxxxxxxxxx", // Auth0 user 'sub' string "site": "saf", // Sitecode where reservation was made @@ -94,6 +94,8 @@ The body of a calendar event follows the JSON format below: Calendar requests are handled at the base URL `https://calendar.photonranch.org/{stage}`, where `{stage}` is `dev` for the dev environment, or `calendar` for the production version. +All datetimes should be formatted yyyy-MM-ddTHH:mmZ (UTC, 24-hour format) + - POST `/newevent` - Description: Create a new reservation on the calendar. - Authorization required: Yes. @@ -127,6 +129,17 @@ Calendar requests are handled at the base URL `https://calendar.photonranch.org/ - Responses: - 200: success. +- POST `/remove-expired-lco-schedule` + - Description: Removes all events at a given site under the following conditions: + - the event `origin` == "lco" + - the event `start` is after (greater than) the specified cutoff_time + - Authorization required: No. + - Request body: + - `site` (string): dictionaries for each calendar event to update + - `cutoff_time` (string): UTC datestring, which is compared against the `start` attribute + - Responses: + - 200: success. + - POST `/delete` - Description: Delete a calendar event given an event_id. - Authorization required: Yes. From 70c1dfc42c80fb32345efb1bc5a34e8655f2350c Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 28 Oct 2024 11:33:12 -0400 Subject: [PATCH 4/8] Add "origin" to event specification in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9dd91e3..2784a85 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ The body of a calendar event follows the JSON format below: "site": "saf", // Sitecode where reservation was made "title": "My Name", // Name of reservation, defaults to username "reservation_type": "realtime", // String, can be "realtime" or "project" + "origin": "ptr", // "ptr" if created on the ptr site, or "lco" if the event was created by the lco scheduler "resourceId": "saf", // Sitecode where reservation was made "project_id": "none", // Or concatenated string of project_name#created_at timestamp "reservation_note": "", // User-supplied comment string, can be empty From c288750bdbaba83bf78da2d6ebd3fae36669ae91 Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 28 Oct 2024 11:43:38 -0400 Subject: [PATCH 5/8] Update lambda permissions to include dynamodb:DescribeTable" --- serverless.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/serverless.yml b/serverless.yml index 9224ecc..6a9324a 100644 --- a/serverless.yml +++ b/serverless.yml @@ -64,6 +64,7 @@ provider: - "dynamodb:DeleteItem" - "dynamodb:Scan" - "dynamodb:Query" + - "dynamodb:DescribeTable" Resource: - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:custom.calendarTableName}*" From 2c2c58e1501fe849b192acadc167aefda96e7a0d Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 28 Oct 2024 11:53:08 -0400 Subject: [PATCH 6/8] mend --- handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler.py b/handler.py index 9aed054..e9d2c5a 100644 --- a/handler.py +++ b/handler.py @@ -443,7 +443,7 @@ def clearExpiredSchedule(event, context): """ event_body = json.loads(event.get("body", "")) remove_expired_scheduler_events(event_body["cutoff_time"], event_body["site"]) - return create_response(200) + return create_response(200, "success") def getSiteEventsInDateRange(event, context): From 2e5ef32c94333298ed4974bfe40786f4e79fd318 Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 28 Oct 2024 12:07:49 -0400 Subject: [PATCH 7/8] Add missing permission for dynamodb:BatchWriteItem" --- serverless.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/serverless.yml b/serverless.yml index 6a9324a..16a82cd 100644 --- a/serverless.yml +++ b/serverless.yml @@ -65,6 +65,7 @@ provider: - "dynamodb:Scan" - "dynamodb:Query" - "dynamodb:DescribeTable" + - "dynamodb:BatchWriteItem" Resource: - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:custom.calendarTableName}*" From dee5a8d650005367569c1113c1eeb35208c42e01 Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Tue, 29 Oct 2024 11:41:43 -0400 Subject: [PATCH 8/8] Return associated project IDs with deleted scheduler events --- handler.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/handler.py b/handler.py index e9d2c5a..6a24b11 100644 --- a/handler.py +++ b/handler.py @@ -128,6 +128,24 @@ def getProject(project_name, created_at): def remove_expired_scheduler_events(cutoff_time, site): + """ Method for deleting calendar events created in response to the LCO scheduler. + + This method takes a site and a cutoff time, and deletes all events that satisfy the following conditions: + - the event belongs to the given site + - the event starts after the cutoff_time (specifically, the event start is greater than the cutoff_time) + - the event origin is 'lco' + It returns an array of project IDs that were associated with the deleted events so that they can be deleted as well. + + Args: + cutoff_time (str): + Formatted yyyy-MM-ddTHH:mmZ (UTC, 24-hour format) + Any events that start before this time are not deleted. + site (str): + Only delete events from the given site (e.g. 'mrc') + + Returns: + (array of str) project IDs for any projects that were connected to deleted events. + """ table = dynamodb.Table(calendar_table_name) index_name = "site-end-index" @@ -162,6 +180,9 @@ def remove_expired_scheduler_events(cutoff_time, site): for item in items: batch.delete_item(Key={k: item[k] for k in key_names if k in item}) + associated_projects = [x["project_id"] for x in items] + return associated_projects + #=========================================# @@ -439,11 +460,11 @@ def clearExpiredSchedule(event, context): UTC datestring (eg. '2022-05-14T17:30:00Z'). All events that start after this will be removed. Returns: - 200 status code + 200 status code, with list of projects that were associated with the deleted events """ event_body = json.loads(event.get("body", "")) - remove_expired_scheduler_events(event_body["cutoff_time"], event_body["site"]) - return create_response(200, "success") + associated_projects = remove_expired_scheduler_events(event_body["cutoff_time"], event_body["site"]) + return create_response(200, json.dumps(associated_projects)) def getSiteEventsInDateRange(event, context):