From 06d71f8631edb83c59baa365a18095fd2d6244f1 Mon Sep 17 00:00:00 2001 From: Jacob Pan Date: Mon, 25 Jul 2022 20:20:41 +0000 Subject: [PATCH 1/3] Add Endpoint to Edit Alerts Add function for endpoint that allows frontend to send date to update the respective alert configuration, and also add an update function to update the SQLite table. BCL-7009 --- alerts_backend/python/app.py | 153 +++++++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 35 deletions(-) diff --git a/alerts_backend/python/app.py b/alerts_backend/python/app.py index 0e4a379..5527a79 100644 --- a/alerts_backend/python/app.py +++ b/alerts_backend/python/app.py @@ -3,6 +3,7 @@ from datetime import datetime from typing import Dict, Any, Tuple, Set +import copy import json import requests from flask import Flask, jsonify, Response, request @@ -11,7 +12,7 @@ from sqlalchemy import (exc, create_engine, MetaData, Table, Column, Integer, Boolean, Text, insert, - Date, DateTime, delete) + Date, DateTime, delete, update) from sqlalchemy.sql import func from sqlalchemy import ( exc, @@ -50,41 +51,42 @@ metadata_obj = MetaData() # Table for alert configurations aeroapi_alert_configurations = Table( - "aeroapi_alert_configurations", - metadata_obj, - Column("fa_alert_id", Integer, primary_key=True), - Column("ident", Text), - Column("origin", Text), - Column("destination", Text), - Column("aircraft_type", Text), - Column("start_date", Date), - Column("end_date", Date), - Column("max_weekly", Integer), - Column("eta", Integer), - Column("arrival", Boolean), - Column("cancelled", Boolean), - Column("departure", Boolean), - Column("diverted", Boolean), - Column("filed", Boolean), - ) + "aeroapi_alert_configurations", + metadata_obj, + Column("fa_alert_id", Integer, primary_key=True), + Column("ident", Text), + Column("origin", Text), + Column("destination", Text), + Column("aircraft_type", Text), + Column("start_date", Date), + Column("end_date", Date), + Column("max_weekly", Integer), + Column("eta", Integer), + Column("arrival", Boolean), + Column("cancelled", Boolean), + Column("departure", Boolean), + Column("diverted", Boolean), + Column("filed", Boolean), +) # Table for POSTed alerts aeroapi_alerts = Table( - "aeroapi_alerts", - metadata_obj, - Column("id", Integer, primary_key=True, autoincrement=True), - Column("time_alert_received", DateTime(timezone=True), server_default=func.now()), # Store time in UTC that the alert was received - Column("long_description", Text), - Column("short_description", Text), - Column("summary", Text), - Column("event_code", Text), - Column("alert_id", Integer), - Column("fa_flight_id", Text), - Column("ident", Text), - Column("registration", Text), - Column("aircraft_type", Text), - Column("origin", Text), - Column("destination", Text) - ) + "aeroapi_alerts", + metadata_obj, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("time_alert_received", DateTime(timezone=True), server_default=func.now()), + # Store time in UTC that the alert was received + Column("long_description", Text), + Column("short_description", Text), + Column("summary", Text), + Column("event_code", Text), + Column("alert_id", Integer), + Column("fa_flight_id", Text), + Column("ident", Text), + Column("registration", Text), + Column("aircraft_type", Text), + Column("origin", Text), + Column("destination", Text) +) def create_tables(): @@ -141,6 +143,24 @@ def delete_from_table(fa_alert_id: int): return 0 +def modify_from_table(fa_alert_id: int, modified_data: Dict[str, Any]): + """ + Updates alert config from SQL Alert Configurations table based on FA Alert ID. + Returns 0 on success, -1 otherwise. + """ + try: + with engine.connect() as conn: + stmt = (update(aeroapi_alert_configurations). + where(aeroapi_alert_configurations.c.fa_alert_id == fa_alert_id)) + conn.execute(stmt, modified_data) + conn.commit() + logger.info(f"Data successfully updated in table {aeroapi_alert_configurations.name}") + except exc.SQLAlchemyError as e: + logger.error(f"SQL error occurred during updating in table {aeroapi_alert_configurations.name}: {e}") + return -1 + return 0 + + def get_alerts_not_from_app(existing_alert_ids: Set[int]): """ Function to get all alert configurations that were not configured @@ -183,6 +203,68 @@ def get_alerts_not_from_app(existing_alert_ids: Set[int]): return alerts_not_from_app +@app.route("/modify", methods=["POST"]) +def modify_alert(): + """ + Function to modify the alert given (with key "fa_alert_id" in the payload). + Modifies the given alert via AeroAPI PUT call and also modifies the respective + alert in the SQLite database. Returns JSON Response in form {"Success": True/False, + "Description": } + """ + r_success: bool = False + r_description: str + # Process json + content_type = request.headers.get("Content-Type") + data: Dict[str, Any] + + if content_type != "application/json": + r_description = "Invalid content sent" + else: + data = request.json + + fa_alert_id = data.pop('fa_alert_id') + + # Make deep copy to send to AeroAPI - needs events in nested dictionary + aeroapi_adjusted_data = copy.deepcopy(data) + aeroapi_adjusted_data["events"] = { + "arrival": aeroapi_adjusted_data.pop('arrival'), + "departure": aeroapi_adjusted_data.pop('departure'), + "cancelled": aeroapi_adjusted_data.pop('cancelled'), + "diverted": aeroapi_adjusted_data.pop('diverted'), + "filed": aeroapi_adjusted_data.pop('filed'), + } + + api_resource = f"/alerts/{fa_alert_id}" + logger.info(f"Making AeroAPI request to PUT {api_resource}") + result = AEROAPI.put(f"{AEROAPI_BASE_URL}{api_resource}", json=aeroapi_adjusted_data) + if result.status_code != 204: + # return to front end the error, decode and clean the response + try: + processed_json = result.json() + r_description = f"Error code {result.status_code} with the following description for alert configuration {fa_alert_id}: {processed_json['detail']}" + except json.decoder.JSONDecodeError: + r_description = f"Error code {result.status_code} for the alert configuration {fa_alert_id} could not be parsed into JSON. The following is the HTML response given: {result.text}" + else: + # Parse into datetime to update in SQLite table + if data["start_date"]: + data["start_date"] = datetime.strptime(data["start_date"], "%Y-%m-%d") + if data["end_date"]: + data["end_date"] = datetime.strptime(data["end_date"], "%Y-%m-%d") + + # Check if data was inserted into database properly + if modify_from_table(fa_alert_id, data) == -1: + r_description = ( + "Error modifying the alert configuration from the SQL Database - since it was modified " + "on AeroAPI but not SQL, this means the alert will still be the original alert on the table - in " + "order to properly modify the alert please look in your SQL database." + ) + else: + r_success = True + r_description = f"Request sent successfully, alert configuration {fa_alert_id} has been updated" + + return jsonify({"Success": r_success, "Description": r_description}) + + @app.route("/delete", methods=["POST"]) def delete_alert(): """ @@ -307,7 +389,8 @@ def handle_alert() -> Tuple[Response, int]: r_status = 200 except KeyError as e: # If value doesn't exist, do not insert into table and produce error - logger.error(f"Alert POST request did not have one or more keys with data. Will process but will return 400: {e}") + logger.error( + f"Alert POST request did not have one or more keys with data. Will process but will return 400: {e}") r_title = "Missing info in request" r_detail = "At least one value to insert in the database is missing in the post request" r_status = 400 From 07efed1d069b120625b4ae2b08af51f65ec8ce7e Mon Sep 17 00:00:00 2001 From: Jacob Pan Date: Mon, 25 Jul 2022 21:25:20 +0000 Subject: [PATCH 2/3] Rename Start and End Quick renaming of fields from frontend to send to AeroAPI BCK-7009 --- alerts_backend/python/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/alerts_backend/python/app.py b/alerts_backend/python/app.py index 5527a79..dd39646 100644 --- a/alerts_backend/python/app.py +++ b/alerts_backend/python/app.py @@ -233,6 +233,9 @@ def modify_alert(): "diverted": aeroapi_adjusted_data.pop('diverted'), "filed": aeroapi_adjusted_data.pop('filed'), } + # Rename start and end again + aeroapi_adjusted_data["start"] = aeroapi_adjusted_data.pop("start_date") + aeroapi_adjusted_data["end"] = aeroapi_adjusted_data.pop("end_date") api_resource = f"/alerts/{fa_alert_id}" logger.info(f"Making AeroAPI request to PUT {api_resource}") From 3cbd58e59550923b6bb903f9e2f8c3b0d1c4ef23 Mon Sep 17 00:00:00 2001 From: Jacob Pan Date: Tue, 26 Jul 2022 22:33:09 +0000 Subject: [PATCH 3/3] Prevent Duplicate Alerts from being Created Add function to check if duplicate alert is trying to be created (that is, if ident, origin, destination, and aircraft type are all the same). If it is do not create the alert and send response to frontend. BCK-7006 --- alerts_backend/python/app.py | 112 +++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 43 deletions(-) diff --git a/alerts_backend/python/app.py b/alerts_backend/python/app.py index dd39646..24f3a2c 100644 --- a/alerts_backend/python/app.py +++ b/alerts_backend/python/app.py @@ -13,7 +13,7 @@ from sqlalchemy import (exc, create_engine, MetaData, Table, Column, Integer, Boolean, Text, insert, Date, DateTime, delete, update) -from sqlalchemy.sql import func +from sqlalchemy.sql import func, and_ from sqlalchemy import ( exc, create_engine, @@ -26,6 +26,7 @@ insert, Date, select, + exists ) AEROAPI_BASE_URL = "https://aeroapi.flightaware.com/aeroapi" @@ -173,10 +174,10 @@ def get_alerts_not_from_app(existing_alert_ids: Set[int]): logger.info(f"Making AeroAPI request to GET {api_resource}") result = AEROAPI.get(f"{AEROAPI_BASE_URL}{api_resource}") if not result: - return None + return [] all_alerts = result.json()["alerts"] if not all_alerts: - return None + return [] alerts_not_from_app = [] for alert in all_alerts: if int(alert["id"]) not in existing_alert_ids: @@ -203,6 +204,27 @@ def get_alerts_not_from_app(existing_alert_ids: Set[int]): return alerts_not_from_app +def check_if_dup(alert_data) -> bool: + """ + Check if given alert is a duplicate alert configured. Do this by checking the + SQLite database. Return True if duplicate, False if not. + """ + try: + with engine.connect() as conn: + stmt = select(aeroapi_alert_configurations).where(and_( + aeroapi_alert_configurations.c.ident == alert_data["ident"], + aeroapi_alert_configurations.c.destination == alert_data["destination"], + aeroapi_alert_configurations.c.origin == alert_data["origin"], + aeroapi_alert_configurations.c.aircraft_type == alert_data["aircraft_type"], + )) + result = conn.execute(stmt) + conn.commit() + return result.all() + except exc.SQLAlchemyError as e: + logger.error(f"SQL error occurred in checking for duplicate alert in table {aeroapi_alert_configurations.name}: {e}") + raise e + + @app.route("/modify", methods=["POST"]) def modify_alert(): """ @@ -437,48 +459,52 @@ def create_alert() -> Response: if "max_weekly" not in data: data["max_weekly"] = 1000 - logger.info(f"Making AeroAPI request to POST {api_resource}") - result = AEROAPI.post(f"{AEROAPI_BASE_URL}{api_resource}", json=data) - if result.status_code != 201: - # return to front end the error, decode and clean the response - try: - processed_json = result.json() - r_description = f"Error code {result.status_code} with the following description: {processed_json['detail']}" - except json.decoder.JSONDecodeError: - r_description = f"Error code {result.status_code} could not be parsed into JSON. The following is the HTML response given: {result.text}" + # Check if alert is duplicate + if check_if_dup(data): + r_description = f"Ticket error: alert has already been configured. Ticket has not been created" else: - # Package created alert and put into database - fa_alert_id = int(result.headers["Location"][8:]) - r_alert_id = fa_alert_id - # Flatten events to insert into database - data["arrival"] = data["events"]["arrival"] - data["departure"] = data["events"]["departure"] - data["cancelled"] = data["events"]["cancelled"] - data["diverted"] = data["events"]["diverted"] - data["filed"] = data["events"]["filed"] - data.pop("events") - # Rename dates to avoid sql keyword "end" issue, and also change to Python datetime.datetime() - # Default to None in case a user directly submits an incomplete payload - data["start_date"] = data.pop("start", None) - data["end_date"] = data.pop("end", None) - # Allow empty strings - if data["start_date"] == "": - data["start_date"] = None - if data["end_date"] == "": - data["end_date"] = None - # Handle if dates are None - accept them but don't parse time - if data["start_date"]: - data["start_date"] = datetime.strptime(data["start_date"], "%Y-%m-%d") - if data["end_date"]: - data["end_date"] = datetime.strptime(data["end_date"], "%Y-%m-%d") - - data["fa_alert_id"] = fa_alert_id - - if insert_into_table(data, aeroapi_alert_configurations) == -1: - r_description = f"Database insertion error, check your database configuration. Alert has still been configured with alert id {r_alert_id}" + logger.info(f"Making AeroAPI request to POST {api_resource}") + result = AEROAPI.post(f"{AEROAPI_BASE_URL}{api_resource}", json=data) + if result.status_code != 201: + # return to front end the error, decode and clean the response + try: + processed_json = result.json() + r_description = f"Error code {result.status_code} with the following description: {processed_json['detail']}" + except json.decoder.JSONDecodeError: + r_description = f"Error code {result.status_code} could not be parsed into JSON. The following is the HTML response given: {result.text}" else: - r_success = True - r_description = f"Request sent successfully with alert id {r_alert_id}" + # Package created alert and put into database + fa_alert_id = int(result.headers["Location"][8:]) + r_alert_id = fa_alert_id + # Flatten events to insert into database + data["arrival"] = data["events"]["arrival"] + data["departure"] = data["events"]["departure"] + data["cancelled"] = data["events"]["cancelled"] + data["diverted"] = data["events"]["diverted"] + data["filed"] = data["events"]["filed"] + data.pop("events") + # Rename dates to avoid sql keyword "end" issue, and also change to Python datetime.datetime() + # Default to None in case a user directly submits an incomplete payload + data["start_date"] = data.pop("start", None) + data["end_date"] = data.pop("end", None) + # Allow empty strings + if data["start_date"] == "": + data["start_date"] = None + if data["end_date"] == "": + data["end_date"] = None + # Handle if dates are None - accept them but don't parse time + if data["start_date"]: + data["start_date"] = datetime.strptime(data["start_date"], "%Y-%m-%d") + if data["end_date"]: + data["end_date"] = datetime.strptime(data["end_date"], "%Y-%m-%d") + + data["fa_alert_id"] = fa_alert_id + + if insert_into_table(data, aeroapi_alert_configurations) == -1: + r_description = f"Database insertion error, check your database configuration. Alert has still been configured with alert id {r_alert_id}" + else: + r_success = True + r_description = f"Request sent successfully with alert id {r_alert_id}" return jsonify({"Alert_id": r_alert_id, "Success": r_success, "Description": r_description})