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: diff --git a/README.md b/README.md index 0133151..2784a85 100644 --- a/README.md +++ b/README.md @@ -77,13 +77,14 @@ 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 "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 @@ -94,6 +95,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 +130,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. diff --git a/handler.py b/handler.py index 8a70d70..6a24b11 100644 --- a/handler.py +++ b/handler.py @@ -127,6 +127,64 @@ def getProject(project_name, created_at): return "Project not found." +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" + + # 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}) + + associated_projects = [x["project_id"] for x in items] + return associated_projects + + + #=========================================# #======= API Endpoints ========# #=========================================# @@ -392,6 +450,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, with list of projects that were associated with the deleted events + """ + event_body = json.loads(event.get("body", "")) + 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): """Return calendar events within a specified date range at a given site. diff --git a/serverless.yml b/serverless.yml index cc2b80e..16a82cd 100644 --- a/serverless.yml +++ b/serverless.yml @@ -64,6 +64,8 @@ provider: - "dynamodb:DeleteItem" - "dynamodb:Scan" - "dynamodb:Query" + - "dynamodb:DescribeTable" + - "dynamodb:BatchWriteItem" Resource: - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:custom.calendarTableName}*" @@ -265,7 +267,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: