mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-09 14:29:49 -04:00
Getting the frontend and backend wired up correctly.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -9,6 +9,7 @@ from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from backend import config
|
||||
from backend.logging import setup_logging
|
||||
@@ -45,8 +46,22 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
|
||||
app_configs["lifespan"] = lifespan
|
||||
|
||||
|
||||
app = FastAPI(**app_configs)
|
||||
|
||||
# Allow CORS for frontend (adjust origins as needed for production)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:3000", # React dev server
|
||||
"http://localhost:8000", # Docker Compose frontend (if served on 8000)
|
||||
"http://frontend:3000", # Docker Compose service name
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(users.router)
|
||||
app.include_router(rooms.router)
|
||||
app.include_router(bookings.router)
|
||||
|
||||
@@ -134,6 +134,7 @@ async def create_db_and_tables() -> None: # pragma: no cover
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
# Python type aliases for lists
|
||||
type UserList = list[User]
|
||||
type RoomList = list[Room]
|
||||
type BookingList = list[Booking]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Routes for booking-related operations in the backend."""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
from fastapi import status
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
@@ -30,25 +30,39 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/bookings", tags=["bookings"])
|
||||
|
||||
|
||||
@router.get("/room/{room_id}", response_model=List[BookingResponse])
|
||||
async def read_bookings_for_room(room_id: int, session: DBSession) -> BookingList:
|
||||
"""Retrieve all bookings for a specific room.
|
||||
_default_date = Query(None, description="Filter bookings by date (YYYY-MM-DD)")
|
||||
|
||||
|
||||
@router.get("/room/{room_id}", response_model=list[BookingResponse])
|
||||
async def read_bookings_for_room(
|
||||
room_id: int,
|
||||
session: DBSession,
|
||||
date: str = _default_date,
|
||||
) -> BookingList:
|
||||
"""Retrieve bookings for a specific room, optionally filtered by date.
|
||||
|
||||
Args:
|
||||
room_id: ID of the room to retrieve bookings for.
|
||||
session: Database session.
|
||||
|
||||
Returns:
|
||||
List of bookings for the room.
|
||||
room_id (int): The ID of the room to retrieve bookings for.
|
||||
session (DBSession): The database session.
|
||||
date (str): Optional date string (YYYY-MM-DD) to filter bookings by start_time.
|
||||
|
||||
Raises:
|
||||
HTTPException: If a database error or unexpected error occurs.
|
||||
HTTPException: If the date format is invalid.
|
||||
|
||||
Returns:
|
||||
BookingList: List of bookings for the room, filtered by date if provided.
|
||||
"""
|
||||
logger.debug(f"Received request to fetch bookings for room_id: {room_id}")
|
||||
logger.debug(
|
||||
f"Received request to fetch bookings for room_id: {room_id} and date: {date}"
|
||||
)
|
||||
try:
|
||||
bookings = await get_bookings_for_room(session, room_id)
|
||||
try:
|
||||
bookings = await get_bookings_for_room(session, room_id, date)
|
||||
except ValueError as ve:
|
||||
raise HTTPException(status_code=422, detail=str(ve)) from ve
|
||||
logger.info(
|
||||
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id}"
|
||||
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id} "
|
||||
f"and date: {date}"
|
||||
)
|
||||
return bookings
|
||||
except SQLAlchemyError as e:
|
||||
|
||||
@@ -11,11 +11,13 @@ from datetime import timezone
|
||||
from typing import Any
|
||||
from typing import Awaitable
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Sequence
|
||||
from typing import TypedDict
|
||||
from typing import Unpack
|
||||
from typing import cast
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
@@ -23,6 +25,15 @@ from sqlalchemy import update
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.models import Booking
|
||||
|
||||
|
||||
# BookingList is a type alias for List[Booking]
|
||||
try:
|
||||
from backend.models import BookingList
|
||||
except ImportError:
|
||||
BookingList = List[Booking]
|
||||
|
||||
from backend import config
|
||||
from backend.models import Booking
|
||||
from backend.models import BookingList
|
||||
@@ -43,30 +54,58 @@ class BookingParams(TypedDict):
|
||||
"""
|
||||
|
||||
|
||||
async def get_bookings_for_room(session: AsyncSession, room_id: int) -> BookingList:
|
||||
"""Retrieve all bookings for a specific room.
|
||||
async def get_bookings_for_room(
|
||||
session: AsyncSession, room_id: int, date: str | None = None
|
||||
) -> BookingList:
|
||||
"""Retrieve bookings for a specific room, optionally filtered by date.
|
||||
|
||||
If a date string (YYYY-MM-DD) is provided, only bookings whose start_time falls on that date
|
||||
are returned. If no date is provided, all bookings for the room are returned.
|
||||
|
||||
Args:
|
||||
session: Database session.
|
||||
room_id: ID of the room to retrieve bookings for.
|
||||
session: The asynchronous database session.
|
||||
room_id: The ID of the room to retrieve bookings for.
|
||||
date: Optional date string (YYYY-MM-DD) to filter bookings by start_time.
|
||||
|
||||
Returns:
|
||||
List of bookings associated with the room.
|
||||
List of bookings for the room, filtered by date if provided.
|
||||
|
||||
Raises:
|
||||
Exception: If any database error occurs.
|
||||
ValueError: If the date string is not in YYYY-MM-DD format.
|
||||
"""
|
||||
logger.debug(f"Entering get_bookings_for_room with room_id: {room_id}")
|
||||
logger.debug(
|
||||
f"Entering get_bookings_for_room with room_id: {room_id} and date: {date}"
|
||||
)
|
||||
try:
|
||||
stmt = select(Booking).where(Booking.room_id == room_id)
|
||||
result = await session.scalars(stmt)
|
||||
bookings = cast(BookingList, result.all())
|
||||
if date:
|
||||
try:
|
||||
date_obj = datetime.strptime(date, "%Y-%m-%d")
|
||||
except ValueError as e:
|
||||
raise ValueError("Invalid date format. Use YYYY-MM-DD.") from e
|
||||
start_of_day = date_obj
|
||||
end_of_day = date_obj + timedelta(days=1)
|
||||
stmt = select(Booking).where(
|
||||
Booking.room_id == room_id,
|
||||
and_(
|
||||
Booking.start_time >= start_of_day,
|
||||
Booking.start_time < end_of_day,
|
||||
),
|
||||
)
|
||||
result = await session.scalars(stmt)
|
||||
bookings = list(result.all())
|
||||
else:
|
||||
stmt = select(Booking).where(Booking.room_id == room_id)
|
||||
result = await session.scalars(stmt)
|
||||
bookings = cast(BookingList, result.all())
|
||||
logger.info(
|
||||
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id}"
|
||||
f"Successfully retrieved {len(bookings)} bookings for room_id: {room_id} "
|
||||
f"and date: {date}"
|
||||
)
|
||||
return bookings
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve bookings for room_id {room_id}: {str(e)}")
|
||||
logger.error(
|
||||
f"Failed to retrieve bookings for room_id {room_id} and date {date}: {str(e)}"
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
logger.debug("Exiting get_bookings_for_room")
|
||||
|
||||
@@ -50,7 +50,7 @@ async def test_get_bookings_for_room_success(
|
||||
}
|
||||
for booking in sample_bookings
|
||||
]
|
||||
mock_get_bookings.assert_called_once_with(ANY, room_id)
|
||||
mock_get_bookings.assert_called_once_with(ANY, room_id, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -76,7 +76,7 @@ async def test_get_bookings_for_room_empty(
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
mock_get_bookings.assert_called_once_with(ANY, room_id)
|
||||
mock_get_bookings.assert_called_once_with(ANY, room_id, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -102,7 +102,7 @@ async def test_get_bookings_for_room_database_error(
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "detail" in response.json()
|
||||
mock_get_bookings.assert_called_once_with(ANY, room_id)
|
||||
mock_get_bookings.assert_called_once_with(ANY, room_id, None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -33,10 +33,7 @@ from backend.services.bookings import update_booking
|
||||
async def test_get_bookings_for_room_success(
|
||||
async_session: AsyncSession, sample_bookings: BookingList, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test successful retrieval of all bookings for a room.
|
||||
|
||||
Verifies that get_bookings_for_room returns the expected list of bookings and constructs
|
||||
the correct SQLAlchemy query.
|
||||
"""Test retrieval of all bookings for a room with no date filter.
|
||||
|
||||
Args:
|
||||
async_session: The asynchronous database session.
|
||||
@@ -58,6 +55,52 @@ async def test_get_bookings_for_room_success(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
|
||||
async def test_get_bookings_for_room_with_date(
|
||||
async_session: AsyncSession, sample_bookings: BookingList, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test retrieval of bookings for a room filtered by date.
|
||||
|
||||
Args:
|
||||
async_session: The asynchronous database session.
|
||||
sample_bookings: The mocked list of bookings to return.
|
||||
mock_logger: The mocked logger instance.
|
||||
"""
|
||||
room_id = 1
|
||||
date = "2025-08-28"
|
||||
mock_scalars_result = AsyncMock()
|
||||
mock_scalars_result.all = MagicMock(return_value=sample_bookings)
|
||||
mock_scalars = AsyncMock(return_value=mock_scalars_result)
|
||||
with patch.object(async_session, "scalars", mock_scalars) as scalars_mock:
|
||||
result: BookingList = await get_bookings_for_room(async_session, room_id, date)
|
||||
assert isinstance(result, list)
|
||||
assert result == sample_bookings
|
||||
scalars_mock.assert_called_once()
|
||||
# Check the query includes the date filter
|
||||
stmt = scalars_mock.call_args.args[0]
|
||||
assert stmt.whereclause is not None
|
||||
assert str(stmt.whereclause).find("start_time >=") != -1
|
||||
assert str(stmt.whereclause).find("start_time <") != -1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
|
||||
async def test_get_bookings_for_room_invalid_date(
|
||||
async_session: AsyncSession, mock_logger: MagicMock
|
||||
) -> None:
|
||||
"""Test that an invalid date string raises ValueError.
|
||||
|
||||
Args:
|
||||
async_session: The asynchronous database session.
|
||||
mock_logger: The mocked logger instance.
|
||||
"""
|
||||
room_id = 1
|
||||
invalid_date = "not-a-date"
|
||||
with pytest.raises(ValueError):
|
||||
await get_bookings_for_room(async_session, room_id, invalid_date)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("mock_logger", ["backend.services.bookings"], indirect=True)
|
||||
async def test_get_bookings_for_room_empty(
|
||||
|
||||
@@ -33,6 +33,8 @@ services:
|
||||
extends:
|
||||
file: compose.yml
|
||||
service: frontend
|
||||
environment:
|
||||
REACT_APP_API_URL: http://localhost:8000
|
||||
volumes:
|
||||
- ./frontend/src:/app/src
|
||||
- ./frontend/public:/app/public
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
expose:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
ENVIRONMENT: production
|
||||
@@ -22,6 +22,8 @@ services:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
REACT_APP_API_URL: http://localhost:8000
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
|
||||
@@ -7,7 +7,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
axios.defaults.baseURL = "http://backend:8000";
|
||||
axios.defaults.baseURL =
|
||||
process.env.REACT_APP_API_URL || "http://localhost:8000";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
|
||||
Reference in New Issue
Block a user