diff --git a/backend/api/endpoints/command.py b/backend/api/endpoints/command.py index 583ea5a..19ca249 100644 --- a/backend/api/endpoints/command.py +++ b/backend/api/endpoints/command.py @@ -22,9 +22,8 @@ def get_commands(db: Session = Depends(get_db)): items = db.exec(query).all() return {"data": items} - @command_router.post("/", response_model=CommandSingleResponse) -def create_command(payload: CommandRequest): +def create_command(payload: CommandRequest, db: Session = Depends(get_db)): """ Creates an item with the given payload in the database and returns this payload after pulling it from the database @@ -32,11 +31,22 @@ def create_command(payload: CommandRequest): @return returns a json object with field of "data" under which there is the payload now pulled from the database """ # TODO:(Member) Implement this endpoint - + # Find number of commands in database + query = select(Command) + items = db.exec(query).all() + + command = Command (**dict(payload)) + + db.add(command) + db.commit() + db.refresh(command) + + return {"data": command} + @command_router.delete("/{id}", response_model=CommandListResponse) -def delete_command(id: int): +def delete_command(id: int, db: Session = Depends(get_db)): """ Deletes the item with the given id if it exists. Otherwise raises a 404 error. @@ -44,3 +54,13 @@ def delete_command(id: int): @return returns the list of commands after deleting the item """ # TODO:(Member) Implement this endpoint + command = db.get(Command, id) + if command is None: + raise HTTPException(status_code=404, detail="Item does not exist") + + db.delete(command) + db.commit() + + query2 = select(Command).where(Command.id != id) + items = db.exec(query2).all() + return {"data": items} diff --git a/backend/api/middlewares/logger_middleware.py b/backend/api/middlewares/logger_middleware.py index 44daa5d..596680d 100644 --- a/backend/api/middlewares/logger_middleware.py +++ b/backend/api/middlewares/logger_middleware.py @@ -3,6 +3,11 @@ from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware +from backend.utils.logging import logger + +from time import perf_counter +from datetime import datetime + class LoggerMiddleware(BaseHTTPMiddleware): async def dispatch( @@ -18,5 +23,14 @@ async def dispatch( @return Response from endpoint """ # TODO:(Member) Finish implementing this method + logger.info(f'{request.method} {request.url.path}') + #logger.info(f"Request Parameters: {request.path_params}") + current_datetime = datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + logger.info(f"Datetime of request: {current_datetime}") + + start = perf_counter() response = await call_next(request) + duration = perf_counter() - start + logger.info(f"Duration: {duration} seconds") + return response diff --git a/backend/data/data_models.py b/backend/data/data_models.py index 68adddb..342901c 100644 --- a/backend/data/data_models.py +++ b/backend/data/data_models.py @@ -33,12 +33,18 @@ def validate_params_format(self): The format of the comma seperated values is "data1,data2" so no spaces between data and the commas. """ # TODO: (Member) Implement this method - return self - + if self.params is None and self.format is None: + return self + elif self.params is None or self.format is None: + raise ValueError("Parameters do not match required format") + elif len(self.params.split(",")) == len(self.format.split(",")): + return self + else: + raise ValueError("Parameters do not match required format") class Command(BaseSQLModel, table=True): """ - An instance of a MainCommand. + An instance off a MainCommand. This table holds the data related to actual commands sent from the ground station up to the OBC. """ diff --git a/frontend/src/data/request.ts b/frontend/src/data/request.ts index d069f36..8ff9f61 100644 --- a/frontend/src/data/request.ts +++ b/frontend/src/data/request.ts @@ -1,6 +1,6 @@ export interface CommandRequest { - name: string + command_type: number, params: string | null - format: string | null + //format: string | null } diff --git a/frontend/src/display/command_api.ts b/frontend/src/display/command_api.ts index f5ce725..3a9eb24 100644 --- a/frontend/src/display/command_api.ts +++ b/frontend/src/display/command_api.ts @@ -20,3 +20,13 @@ export const getCommands = async (): Promise => { * @param id: command to delete * @returns Promise: list of commands after the command with the given id was deleted */ + +export const deleteCommand = async(id: number): Promise => { + try { + const { data } = await axios.delete(`${API_URL}/commands/${id}`) + return data; + } catch (error) { + console.error(error) + throw error + } +} diff --git a/frontend/src/display/table.tsx b/frontend/src/display/table.tsx index 9f95a77..2fee949 100644 --- a/frontend/src/display/table.tsx +++ b/frontend/src/display/table.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react" import { CommandResponse } from "../data/response" -import { getCommands } from "./command_api" +import { getCommands, deleteCommand } from "./command_api" import CommandRow from "./row" const CommandTable = () => { @@ -19,7 +19,12 @@ const CommandTable = () => { return () => { // TODO: (Member) Handle delete logic here // You will need to create a function in `command_api.ts` before you can finish this part. + const deleteCommandFn = async () => { + const data = await deleteCommand(id); + setCommands(data.data) + } + deleteCommandFn(); } } diff --git a/frontend/src/input/command_input.tsx b/frontend/src/input/command_input.tsx index 2cdbb5e..f19f467 100644 --- a/frontend/src/input/command_input.tsx +++ b/frontend/src/input/command_input.tsx @@ -1,30 +1,120 @@ import "./command_input.css" +import { useState, useEffect } from "react" +import { MainCommandResponse } from "../data/response" +import { createCommand, getMainCommands } from "./input_api" + const CommandInput = () => { // TODO: (Member) Setup state and useEffect calls here + const [mainCommands, setMainCommands] = useState([]) + const [commandType, setCommandType] = useState(null) + const [params, setParams] = useState>(new Map()); + + useEffect(() => { + const setMainCommandsFn = async () => { + const data = await getMainCommands() + if(data.data.length === 0) { + alert("Error occurred. Please try again later.") + return + } + setMainCommands(data.data) + setCommandType(data.data[0]) + + const parameters = data.data[0].params?.split(",") || [] + let paramsObject : Map = new Map() + + for(const param of parameters) { + paramsObject.set(param, "") + } + setParams(paramsObject); + } + setMainCommandsFn(); + }, []) + + + const handleSubmit = (e: React.FormEvent) => { + // TODO:(Member) Submit to your post endpoint + e.preventDefault(); + + //Check if parameters are valid + const allParams = commandType?.params?.split(",") || []; + const paramsList = [] + + const missingParams = [] + for (const param of allParams) { + if (!params.get(param)) { + missingParams.push(param) + } + paramsList.push(params.get(param)) + } + if(missingParams.length !== 0) { + alert(`Parameters missing: ${missingParams.join(", ")}`) + return; + } + + + const reqBody = { + command_type: commandType?.id || 0, + params: paramsList.join(",") + }; + + + const createCommandFn = async () => { + await createCommand(reqBody); + window.location.reload(); + } + createCommandFn(); + } + + const changeCommandType = (e: React.ChangeEvent) => { + const cmdType = mainCommands.find(cmd => cmd.id === +e.target.value) + if(!cmdType) { + alert("Error occurred. Please try again later.") + return + } + setCommandType(cmdType) + const parameters = cmdType?.params?.split(",") || [] + let paramsObject : Map = new Map() + for(const param of parameters) { + paramsObject.set(param, "") + } + setParams(paramsObject) + } + + const changeParam = (e: React.ChangeEvent) => { + const param = e.target.id + const value = e.target.value - const handleSubmit = () => { - // TODO:(Member) Submit to your post endpoint + setParams(prevParams => { + const newParams = new Map(prevParams); + newParams.set(param, value); + return newParams; + }) } return ( <> -
+
- + {mainCommands.map(cmd => ())}
- {/* TODO: (Member) Add input handling here if the selected command has a param input*/} - + + {/* TODO: (Member) Add input handling here if the selected command has a param input*/} + {commandType && + commandType.params?.split(",").map((param) => ())} + +
- + ) } -export default CommandInput; +export default CommandInput; \ No newline at end of file diff --git a/test/backend/test_api.py b/test/backend/test_api.py index c859834..3b9546e 100644 --- a/test/backend/test_api.py +++ b/test/backend/test_api.py @@ -3,6 +3,8 @@ from backend.data.enums import CommandStatus from backend.utils.time import to_unix_time +from datetime import datetime + def test_get_commands(fastapi_test_client: TestClient, commands_json): with fastapi_test_client as client: @@ -24,8 +26,10 @@ def test_create_command(fastapi_test_client: TestClient): assert result.get("status") == CommandStatus.PENDING.value assert result.get("params") == "123456789" # TODO: Figure out a better way to check the times - assert result.get("created_on") - assert result.get("updated_on") + created_on_dt = datetime.fromisoformat(result.get("created_on")) + updated_on_dt = datetime.fromisoformat(result.get("updated_on")) + assert isinstance(created_on_dt, datetime) + assert isinstance(updated_on_dt, datetime) def test_delete_command_fail(fastapi_test_client: TestClient): with fastapi_test_client as client: