Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions backend/api/endpoints/command.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select

from backend.api.models.request_model import CommandRequest
Expand All @@ -17,29 +17,43 @@ def get_commands(db: Session = Depends(get_db)):

:return: Returns a list of commands
"""
query = select(Command)
items = db.exec(query).all()

items = db.exec(select(Command)).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

:param payload: The data used to create an item
: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

# COMPLETED:(Member) Implement this endpoint
payload = Command.model_validate(payload.model_dump())
db.add(payload)
db.commit()
db.refresh(payload)
return {"data": payload}


@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.

:param id: The id of the item to delete
:return: returns the list of commands after deleting the item
"""
# TODO:(Member) Implement this endpoint
# COMPLETED:(Member) Implement this endpoint

command = db.get(Command, id)
if not command: # raise 404 if command does not exist
raise HTTPException(status_code=404, detail="Command not found")

db.delete(command)
db.commit()

return get_commands(db)
2 changes: 1 addition & 1 deletion backend/api/endpoints/main_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ def get_main_commands(db: Session = Depends(get_db)):
"""
query = select(MainCommand)
items = db.exec(query).all()
return {"data": items}
return {"data": items}
25 changes: 23 additions & 2 deletions backend/api/middlewares/logger_middleware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from collections.abc import Callable
from datetime import datetime
from typing import Any
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware

from loguru import logger

class LoggerMiddleware(BaseHTTPMiddleware):
async def dispatch(
Expand All @@ -16,7 +17,27 @@ async def dispatch(
:param request: Request received to this middleware from client (it is supplied by FastAPI)
:param call_next: Endpoint or next middleware to be called (if any, this is the next middleware in the chain of middlewares, it is supplied by FastAPI)
:return: Response from endpoint


personal comments:
must override dispatch method of BaseHTTPMiddleware with custom logging
request -> LoggerMiddleware -> endpoint -> response
"""
# TODO:(Member) Finish implementing this method

# COMPLETED:(Member) Finish implementing this method
start_time = datetime.now()

try:
body = await request.json()

except Exception:
body_bytes = await request.body()
body = body_bytes.decode("utf-8") if body_bytes else None

logger.info(f"REQUEST: {request.method} {request.url} - PARAMS: {body}")

response = await call_next(request)
duration = datetime.now() - start_time
logger.info(f"RESPONSE: {response.status_code} {request.url} ({duration.total_seconds():.2f}s)")
return response

16 changes: 14 additions & 2 deletions backend/data/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,21 @@ def validate_params_format(self):
In either of these cases return self. Otherwise raise a ValueError.
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
# COMPLETED: (Member) Implement this method

if self.params is None and self.format is None:
return self

if (self.params is None) != (self.format is None):
raise ValueError("Params and format must both be None or both provided.")

params_list = (self.params or "").strip().split(",")
format_list = (self.format or "").strip().split(",")

if len(params_list) != len(format_list):
raise ValueError("Params and format must have the same number of values.")

return self

class Command(BaseSQLModel, table=True):
"""
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/display/command_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ export const getCommands = async (): Promise<CommandListResponse> => {
}

/**
* TODO: (Member) Create a deleteCommand API function based on the following specs. You should be using axios to make the API call
* COMPLETED: (Member) Create a deleteCommand API function based on the following specs. You should be using axios to make the API call
*
* Deletes the command with the given id on the backend and returns the list of commands after its deletion.
*
* @param id: command to delete
* @returns Promise<CommandListResponse>: list of commands after the command with the given id was deleted
*/

export const deleteCommand = async (id: number): Promise<CommandListResponse> => {
try {
const { data } = await axios.delete<CommandListResponse>(`${API_URL}/commands/${id}`)
return data;
} catch (error) {
console.error(`Error deleting command: ${error}`);
throw error
}
}
11 changes: 8 additions & 3 deletions frontend/src/display/table.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommandResponse } from "../data/response"
import CommandRow from "./row"
import { deleteCommand} from "../display/command_api"

interface CommandTableProp {
commands: CommandResponse[],
Expand All @@ -13,9 +14,13 @@ const CommandTable = ({

const handleDelete = (id: number) => {
return async () => {
// TODO: (Member) You will need to create a function in `command_api.ts` so you can delete a command.
const data = await deleteCommand(id)
setCommands(data.data)
// COMPLETED: (Member) You will need to create a function in `command_api.ts` so you can delete a command.
try {
const updatedCommands = await deleteCommand(id);
setCommands(updatedCommands.data);
} catch (error) {
console.error(`Error deleting command with id ${id}: ${error}`);
}
}
}

Expand Down
54 changes: 46 additions & 8 deletions frontend/src/input/command_input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { CommandResponse, MainCommandResponse } from "../data/response"
import "./command_input.css"
import { createCommand, getMainCommands } from "./input_api";
import { CommandRequest } from "../data/request";

interface CommandInputProp {
setCommands: React.Dispatch<React.SetStateAction<CommandResponse[]>>
Expand All @@ -9,9 +11,17 @@ interface CommandInputProp {
const CommandInput = ({ setCommands }: CommandInputProp) => {
const [selectedCommand, setSelectedCommand] = useState<MainCommandResponse | null>(null);
const [parameters, setParameters] = useState<{ [key: string]: string }>({});
// TODO: (Member) Setup anymore states if necessary
// COMPLETED: (Member) Setup anymore states if necessary
const [mainCommands, setMainCommands] = useState<MainCommandResponse[]>([]);

// TODO: (Member) Fetch MainCommands in a useEffect
// COMPLETED: (Member) Fetch MainCommands in a useEffect
useEffect(() => {
const fetchMainCommands = async () => {
const response = await getMainCommands();
setMainCommands(response.data);
};
fetchMainCommands();
}, []);

const handleParameterChange = (param: string, value: string): void => {
setParameters((prev) => ({
Expand All @@ -21,20 +31,48 @@ const CommandInput = ({ setCommands }: CommandInputProp) => {
}

const handleSubmit = async (e: React.FormEvent) => {
// TODO:(Member) Submit to your post endpoint
// COMPLETED:(Member) Submit to your post endpoint

e.preventDefault(); // preventing default behavior like the page reloading when form is submitted

//validate that a command is selected
if (!selectedCommand) {
alert("Please select a command type.");
return;
}

try {
const commandParams = Object.values(parameters).join(",");
const commandData: CommandRequest = {
command_type: selectedCommand.id,
params: commandParams || null,
};

const response = await createCommand(commandData);
const createdCommand = response.data;
setCommands((prev) => [...prev, createdCommand]);

setSelectedCommand(null);
setParameters({});
} catch (error) {
console.error("Error creating command:", error);
}
};

return (
<>
<form onSubmit={handleSubmit}>
<div className="spreader">
<div>
<label>Command Type: </label>
<select>{/* TODO: (Member) Display the list of commands based on the get commands request.
{/* COMPLETED: (Member) Display the list of commands based on the get commands request.
It should update the `selectedCommand` field when selecting one.*/}
<option value={"1"}>Command 1</option>
<option value={"2"}>Command 2</option>
<option value={"3"}>Command 3</option>
<select onChange={(e) => setSelectedCommand(mainCommands.find(cmd => cmd.id === Number(e.target.value)) || null)}>
{mainCommands.map((cmd) => (
<option key={cmd.id} value={cmd.id}>
{cmd.name}
</option>
))}
</select>
</div>
{selectedCommand?.params?.split(",").map((param) => (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/input/input_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CommandSingleResponse, MainCommandListResponse } from "../data/response

export const createCommand = async (requestData: CommandRequest): Promise<CommandSingleResponse> => {
try {
const { data } = await axios.post<CommandSingleResponse>(`${API_URL}/commands`, requestData);
const { data } = await axios.post(`${API_URL}/commands`, requestData);
return data
} catch (error) {
console.error(`Error creating command: ${error}`);
Expand Down
14 changes: 11 additions & 3 deletions test/backend/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,17 @@ def test_delete_command(fastapi_test_client: TestClient, default_datetime):
assert result.get("command_type") == 2
assert result.get("status") == CommandStatus.PENDING.value
assert result.get("params") == f"1,{to_unix_time(default_datetime)}"
# TODO: Figure out a better way to check the times
assert result.get("created_on")
assert result.get("updated_on")
# COMPLETED: Figure out a better way to check the times

# Check to make sure the dates are valid
created_on_dt = datetime.fromisoformat(result.get("created_on"))
updated_on_dt = datetime.fromisoformat(result.get("updated_on"))
assert isinstance(created_on_dt, datetime) # checks whether timestamp format is valid
assert isinstance(updated_on_dt, datetime)

# Check to make sure the timestamp ordering is valid
assert created_on_dt <= updated_on_dt
assert updated_on_dt <= datetime.now()


def test_main_commands(fastapi_test_client: TestClient):
Expand Down