Add guests API with CRUD operations and tests
All checks were successful
Build and Push Docker Images / changes (push) Successful in 4s
Build and Push Docker Images / build-backend (push) Successful in 50s
Build and Push Docker Images / build-frontend (push) Has been skipped

Introduce a new API for managing event guests, including endpoints for creating, reading, updating, deleting, and changing guest status. Added corresponding Pydantic schemas, database CRUD logic, and extensive test coverage to validate functionality. Integrated the guests API under the events router.
This commit is contained in:
2025-03-15 02:09:17 +01:00
parent 12161e048d
commit 270bfda1f8
5 changed files with 258 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.schemas.guests import GuestCreate, GuestUpdate, GuestRead
from app.crud.guest import guest_crud
from typing import List
import uuid
from app.core.database import get_db
from app.models import GuestStatus
router = APIRouter()
@router.post("/", response_model=GuestRead, operation_id="create_guest")
def create_guest(guest_in: GuestCreate, db: Session = Depends(get_db)):
guest = guest_crud.create(db, obj_in=guest_in)
return guest
@router.get("/{guest_id}", response_model=GuestRead, operation_id="get_guest")
def read_guest(guest_id: uuid.UUID, db: Session = Depends(get_db)):
guest = guest_crud.get(db, guest_id)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
return guest
@router.get("/", response_model=List[GuestRead], operation_id="get_guests")
def read_guests(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
guests = guest_crud.get_multi(db, skip=skip, limit=limit)
return guests
@router.put("/{guest_id}", response_model=GuestRead, operation_id="update_guest")
def update_guest(guest_id: uuid.UUID, guest_in: GuestUpdate, db: Session = Depends(get_db)):
guest = guest_crud.get(db, guest_id)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
guest = guest_crud.update(db, db_obj=guest, obj_in=guest_in)
return guest
@router.delete("/{guest_id}", response_model=GuestRead, operation_id="delete_guest")
def delete_guest(guest_id: uuid.UUID, db: Session = Depends(get_db)):
guest = guest_crud.get(db, guest_id)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
guest = guest_crud.remove(db, guest_id=str(guest_id))
return guest
@router.patch("/{guest_id}/status", response_model=GuestRead)
def set_guest_status(guest_id: uuid.UUID, status: GuestStatus, db: Session = Depends(get_db)):
guest = guest_crud.update_status(db, guest_id=guest_id, status=status)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
return guest

View File

@@ -21,9 +21,12 @@ from app.schemas.events import (
EventResponse, Event,
)
from app.api.routes.events import guests
logger = logging.getLogger(__name__)
events_router = APIRouter()
events_router.include_router(guests.router, prefix="/guests", tags=["guests"])
def validate_event_access(
*,

46
backend/app/crud/guest.py Normal file
View File

@@ -0,0 +1,46 @@
from app.crud.base import CRUDBase
from app.models.guest import Guest
from app.schemas.guests import GuestCreate, GuestUpdate
from sqlalchemy.orm import Session
from typing import Optional
from app.models.guest import GuestStatus
from datetime import datetime, timezone
import uuid
class CRUDGuest(CRUDBase[Guest, GuestCreate, GuestUpdate]):
def create(self, db, obj_in: GuestCreate):
db_guest = Guest(
event_id=uuid.UUID(obj_in.event_id) if isinstance(obj_in.event_id, str) else obj_in.event_id, # explicit casting
invited_by=uuid.UUID(obj_in.invited_by) if isinstance(obj_in.invited_by, str) else obj_in.invited_by,
full_name=obj_in.full_name,
email=obj_in.email,
invitation_code=obj_in.invitation_code
)
db.add(db_guest)
db.commit()
db.refresh(db_guest)
return db_guest
def get_by_invitation_code(self, db: Session, invitation_code: str) -> Optional[Guest]:
return db.query(Guest).filter(Guest.invitation_code == invitation_code).first()
def update_status(self, db: Session, guest_id: uuid.UUID, status: GuestStatus):
guest = self.get(db, guest_id)
if guest:
guest.status = status
guest.response_date = datetime.now(timezone.utc)
db.commit()
db.refresh(guest)
return guest
def remove(self, db: Session, *, guest_id: str) -> Guest:
guest_id = uuid.UUID(guest_id) if isinstance(guest_id, str) else guest_id
obj = db.query(self.model).get(guest_id)
db.delete(obj)
db.commit()
return obj
guest_crud = CRUDGuest(Guest)

View File

@@ -0,0 +1,48 @@
import uuid
from pydantic import BaseModel, EmailStr, ConfigDict
from datetime import datetime
from typing import Optional, Any, Dict
from app.models.guest import GuestStatus
from uuid import UUID
class GuestBase(BaseModel):
event_id: UUID
invited_by: UUID
user_id: Optional[UUID] = None
full_name: str
email: Optional[EmailStr] = None
phone: Optional[str] = None
max_additional_guests: Optional[int] = 0
dietary_restrictions: Optional[str] = None
notes: Optional[str] = None
custom_fields: Optional[Dict[str, Any]] = None
can_bring_guests: Optional[bool] = False
class GuestCreate(GuestBase):
invitation_code: str
class GuestUpdate(BaseModel):
full_name: Optional[str] = None
email: Optional[EmailStr] = None
phone: Optional[str] = None
status: Optional[GuestStatus] = None
max_additional_guests: Optional[int] = None
actual_additional_guests: Optional[int] = None
dietary_restrictions: Optional[str] = None
notes: Optional[str] = None
custom_fields: Optional[Dict[str, Any]] = None
is_blocked: Optional[bool] = None
can_bring_guests: Optional[bool] = None
class GuestRead(GuestBase):
id: UUID
status: GuestStatus
invitation_sent_at: Optional[datetime] = None
response_date: Optional[datetime] = None
actual_additional_guests: int
is_blocked: bool
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,104 @@
import uuid
import pytest
from app.api.routes.events.guests import router as guests_router
from app.models import GuestStatus
@pytest.fixture
def guest_data():
return {
"event_id": str(uuid.uuid4()), # Correctly generates actual UUID objects
"invited_by": str(uuid.uuid4()),
"full_name": "John Doe",
"email": "john.doe@example.com",
"invitation_code": "INVITE12345",
"can_bring_guests": False
}
class TestGuestsRouter:
@pytest.fixture(autouse=True)
def setup_method(self, create_test_client, db_session, mock_user, guest_fixture):
self.client = create_test_client(
router=guests_router,
prefix="/guests",
db_session=db_session,
user=mock_user
)
self.db_session = db_session
self.mock_user = mock_user
self.endpoint = "/guests"
self.guest = guest_fixture
def test_create_guest_success(self, guest_data):
response = self.client.post(self.endpoint, json=guest_data)
assert response.status_code == 200
data = response.json()
assert data["full_name"] == guest_data["full_name"]
assert data["email"] == guest_data["email"]
def test_create_guest_missing_required_fields_fails(self):
incomplete_payload = {
"email": "john.doe@example.com"
}
response = self.client.post(self.endpoint, json=incomplete_payload)
assert response.status_code == 422
def test_read_guest_success(self, guest_fixture):
response = self.client.get(f"{self.endpoint}/{guest_fixture.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(guest_fixture.id)
assert data["full_name"] == guest_fixture.full_name
def test_read_guest_not_found(self):
random_uuid = str(uuid.uuid4())
response = self.client.get(f"{self.endpoint}/{random_uuid}")
assert response.status_code == 404
assert response.json()["detail"] == "Guest not found"
def test_update_guest_success(self):
update_data = {"full_name": "Jane Doe"}
response = self.client.put(f"{self.endpoint}/{self.guest.id}", json=update_data)
assert response.status_code == 200
assert response.json()["full_name"] == "Jane Doe"
def test_update_guest_not_found_fails(self):
fake_uuid = str(uuid.uuid4())
update_data = {"full_name": "Nobody"}
response = self.client.put(f"{self.endpoint}/{fake_uuid}", json=update_data)
assert response.status_code == 404
def test_delete_guest_success(self):
response = self.client.delete(f"{self.endpoint}/{self.guest.id}")
assert response.status_code == 200
assert response.json()["id"] == str(self.guest.id)
def test_delete_guest_not_found_fails(self):
fake_uuid = str(uuid.uuid4())
response = self.client.delete(f"{self.endpoint}/{fake_uuid}")
assert response.status_code == 404
assert response.json()["detail"] == "Guest not found"
def test_list_guests_success(self):
response = self.client.get(self.endpoint)
assert response.status_code == 200
assert isinstance(response.json(), list)
assert any(g["id"] == str(self.guest.id) for g in response.json())
def test_guest_update_status_success(self):
response = self.client.patch(
f"{self.endpoint}/{self.guest.id}/status",
params={"status": "confirmed"}
)
assert response.status_code == 200
assert response.json()["status"] == GuestStatus.CONFIRMED.value
def test_guest_status_update_invalid_status_fails(self):
response = self.client.patch(
f"{self.endpoint}/{self.guest.id}/status",
params={"status": "invalid_status"}
)
assert response.status_code == 422