Fixe (partially) gift tests and api

This commit is contained in:
2025-03-16 15:07:31 +01:00
parent 4ef202cc5a
commit b5f1c7ddcb
3 changed files with 212 additions and 83 deletions

View File

@@ -1,25 +1,25 @@
from typing import List, Optional, Dict, Any, Union from typing import List, Optional, Dict, Any
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Path from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.dependencies.auth import get_current_active_user, get_current_user from app.api.dependencies.auth import get_current_active_user, get_current_user
from app.core.database import get_db
from app.crud.event import event_crud
from app.crud.gift import gift_item_crud, gift_category_crud, gift_purchase_crud, event_gift_category_crud from app.crud.gift import gift_item_crud, gift_category_crud, gift_purchase_crud, event_gift_category_crud
from app.crud.guest import guest_crud from app.crud.guest import guest_crud
from app.crud.event import event_crud from app.models.gift import GiftStatus, EventGiftCategory, GiftItem as GiftItemModel
from app.models.gift import GiftStatus, GiftPriority, EventGiftCategory, GiftItem
from app.models.user import User from app.models.user import User
from app.schemas.gifts import ( from app.schemas.gifts import (
GiftItem, GiftItemCreate, GiftItemUpdate, GiftItem, GiftItemCreate, GiftItemUpdate,
GiftCategory, GiftCategoryCreate, GiftCategoryUpdate, GiftCategory, GiftCategoryCreate, GiftCategoryUpdate,
GiftPurchase, GiftPurchaseCreate, GiftPurchaseUpdate, GiftPurchase, GiftPurchaseCreate, EventGiftCategoryCreate
EventGiftCategoryCreate, EventGiftCategoryUpdate, EventGiftCategoryInDB
) )
from app.core.database import get_db
router = APIRouter() router = APIRouter()
# ===== GIFT CATEGORIES ===== # # ===== GIFT CATEGORIES ===== #
@router.post("/categories/", response_model=GiftCategory) @router.post("/categories/", response_model=GiftCategory)
@@ -51,7 +51,7 @@ def create_gift_category(
event_id=event_id, event_id=event_id,
category_id=category.id, category_id=category.id,
display_order=0, # Default display order display_order=0, # Default display order
is_visible=True # Default visibility is_visible=True # Default visibility
) )
event_gift_category_crud.create(db, obj_in=association_data) event_gift_category_crud.create(db, obj_in=association_data)
@@ -90,37 +90,70 @@ def read_gift_categories(
db, event_id=event_id, skip=skip, limit=limit, include_hidden=include_hidden db, event_id=event_id, skip=skip, limit=limit, include_hidden=include_hidden
) )
# Enhance categories with display information from the association # Create a list to hold the enhanced category responses
enhanced_categories = []
for category in categories: for category in categories:
# Get the association to access display_order and is_visible # Get the association to access display_order and is_visible
association = event_gift_category_crud.get( association = event_gift_category_crud.get(
db, event_id=event_id, category_id=category.id db, event_id=event_id, category_id=category.id
) )
# Default values
display_order = None
is_visible = None
if association: if association:
category.display_order = association.display_order display_order = association.display_order
category.is_visible = association.is_visible is_visible = association.is_visible
# Calculate statistics for this event # Calculate statistics for this event
total_gifts = 0 total_gifts = 0
available_gifts = 0 available_gifts = 0
if category.gifts: gifts_list = None
for gift in category.gifts:
# If include_gifts is true, fetch gift items for the category
if include_gifts:
gifts = gift_item_crud.get_multi_by_event(
db, event_id=event_id, category_id=category.id, include_hidden=include_hidden
)
gifts_list = gifts
# Calculate statistics
for gift in gifts:
if gift.event_id == event_id: if gift.event_id == event_id:
total_gifts += 1 total_gifts += 1
if gift.status == GiftStatus.AVAILABLE and gift.is_visible: if gift.status == GiftStatus.AVAILABLE and gift.is_visible:
available_gifts += 1 available_gifts += 1
else:
# Calculate statistics without fetching all gifts
if category.gifts:
for gift in category.gifts:
if gift.event_id == event_id:
total_gifts += 1
if gift.status == GiftStatus.AVAILABLE and gift.is_visible:
available_gifts += 1
category.total_gifts = total_gifts # Create a new category response with the calculated values
category.available_gifts = available_gifts category_data = {
**category.__dict__,
"display_order": display_order,
"is_visible": is_visible,
"total_gifts": total_gifts,
"available_gifts": available_gifts
}
# If include_gifts is true, fetch gift items for each category if gifts_list is not None:
if include_gifts: category_data["gifts"] = gifts_list
for category in categories:
gifts = gift_item_crud.get_multi_by_event( # Remove SQLAlchemy state attributes
db, event_id=event_id, category_id=category.id, include_hidden=include_hidden if "_sa_instance_state" in category_data:
) del category_data["_sa_instance_state"]
# Set the gifts attribute which is initially None in the model
setattr(category, "gifts", gifts) enhanced_category = GiftCategory(**category_data)
enhanced_categories.append(enhanced_category)
# Replace the original categories list with the enhanced one
categories = enhanced_categories
return categories return categories
@@ -141,11 +174,12 @@ def read_gift_category(
if not category: if not category:
raise HTTPException(status_code=404, detail="Gift category not found") raise HTTPException(status_code=404, detail="Gift category not found")
# Initialize event-specific properties # Default values for event-specific properties
category.display_order = None display_order = None
category.is_visible = None is_visible = None
category.total_gifts = None total_gifts = None
category.available_gifts = None available_gifts = None
gifts_list = None
# If event_id is provided, get event-specific information # If event_id is provided, get event-specific information
if event_id: if event_id:
@@ -163,9 +197,9 @@ def read_gift_category(
if not association: if not association:
raise HTTPException(status_code=404, detail="Category not associated with this event") raise HTTPException(status_code=404, detail="Category not associated with this event")
# Set event-specific display properties # Get event-specific display properties
category.display_order = association.display_order display_order = association.display_order
category.is_visible = association.is_visible is_visible = association.is_visible
# Calculate statistics for this event # Calculate statistics for this event
total_gifts = 0 total_gifts = 0
@@ -177,8 +211,7 @@ def read_gift_category(
db, event_id=event_id, category_id=category.id, db, event_id=event_id, category_id=category.id,
include_hidden=current_user is not None # Only include hidden for logged-in users include_hidden=current_user is not None # Only include hidden for logged-in users
) )
# Set the gifts attribute gifts_list = gifts
setattr(category, "gifts", gifts)
# Calculate statistics # Calculate statistics
for gift in gifts: for gift in gifts:
@@ -187,23 +220,39 @@ def read_gift_category(
available_gifts += 1 available_gifts += 1
else: else:
# Calculate statistics without fetching all gifts # Calculate statistics without fetching all gifts
gifts_query = db.query(GiftItem).filter( gifts_query = db.query(GiftItemModel).filter(
GiftItem.event_id == event_id, GiftItemModel.event_id == event_id,
GiftItem.category_id == category_id GiftItemModel.category_id == category_id
) )
total_gifts = gifts_query.count() total_gifts = gifts_query.count()
available_gifts = gifts_query.filter( available_gifts = gifts_query.filter(
GiftItem.status == GiftStatus.AVAILABLE, GiftItemModel.status == GiftStatus.AVAILABLE,
GiftItem.is_visible == True GiftItemModel.is_visible == True
).count() ).count()
category.total_gifts = total_gifts
category.available_gifts = available_gifts
elif include_gifts: elif include_gifts:
# If no event_id but include_gifts is true, just get all gifts for this category # If no event_id but include_gifts is true, just get all gifts for this category
# This is less useful without event context but included for completeness # This is less useful without event context but included for completeness
gifts = db.query(GiftItem).filter(GiftItem.category_id == category_id).all() gifts = db.query(GiftItem).filter(GiftItem.category_id == category_id).all()
setattr(category, "gifts", gifts) gifts_list = gifts
# Create a new category response with the calculated values
category_data = {
**category.__dict__,
"display_order": display_order,
"is_visible": is_visible,
"total_gifts": total_gifts,
"available_gifts": available_gifts
}
if gifts_list is not None:
category_data["gifts"] = gifts_list
# Remove SQLAlchemy state attributes
if "_sa_instance_state" in category_data:
del category_data["_sa_instance_state"]
# Create a new category instance with the enhanced data
category = GiftCategory(**category_data)
return category return category
@@ -227,11 +276,11 @@ def update_gift_category(
# Update the category itself # Update the category itself
updated_category = gift_category_crud.update(db, db_obj=category, obj_in=category_in) updated_category = gift_category_crud.update(db, db_obj=category, obj_in=category_in)
# Initialize event-specific properties for the response # Default values for event-specific properties
updated_category.display_order = None display_order = None
updated_category.is_visible = None is_visible = None
updated_category.total_gifts = None total_gifts = None
updated_category.available_gifts = None available_gifts = None
# If event_id is provided, update the event-specific settings # If event_id is provided, update the event-specific settings
if event_id: if event_id:
@@ -252,7 +301,7 @@ def update_gift_category(
event_id=event_id, event_id=event_id,
category_id=category_id, category_id=category_id,
display_order=0, # Default display order display_order=0, # Default display order
is_visible=True # Default visibility is_visible=True # Default visibility
) )
association = event_gift_category_crud.create(db, obj_in=association_data) association = event_gift_category_crud.create(db, obj_in=association_data)
else: else:
@@ -268,21 +317,37 @@ def update_gift_category(
db, db_obj=association, obj_in=association_update db, db_obj=association, obj_in=association_update
) )
# Set event-specific properties for the response # Get event-specific properties for the response
updated_category.display_order = association.display_order display_order = association.display_order
updated_category.is_visible = association.is_visible is_visible = association.is_visible
# Calculate statistics for this event # Calculate statistics for this event
gifts_query = db.query(GiftItem).filter( gifts_query = db.query(GiftItem).filter(
GiftItem.event_id == event_id, GiftItem.event_id == event_id,
GiftItem.category_id == category_id GiftItem.category_id == category_id
) )
updated_category.total_gifts = gifts_query.count() total_gifts = gifts_query.count()
updated_category.available_gifts = gifts_query.filter( available_gifts = gifts_query.filter(
GiftItem.status == GiftStatus.AVAILABLE, GiftItem.status == GiftStatus.AVAILABLE,
GiftItem.is_visible == True GiftItem.is_visible == True
).count() ).count()
# Create a new category response with the calculated values
category_data = {
**updated_category.__dict__,
"display_order": display_order,
"is_visible": is_visible,
"total_gifts": total_gifts,
"available_gifts": available_gifts
}
# Remove SQLAlchemy state attributes
if "_sa_instance_state" in category_data:
del category_data["_sa_instance_state"]
# Create a new category instance with the enhanced data
updated_category = GiftCategory(**category_data)
return updated_category return updated_category
@@ -303,8 +368,12 @@ def delete_gift_category(
if not category: if not category:
raise HTTPException(status_code=404, detail="Gift category not found") raise HTTPException(status_code=404, detail="Gift category not found")
# Make a copy of the category for the response # Default values for event-specific properties
category_copy = GiftCategory.model_validate(category) display_order = None
is_visible = None
total_gifts = None
available_gifts = None
gifts_list = None
if event_id: if event_id:
# Check if event exists # Check if event exists
@@ -324,13 +393,23 @@ def delete_gift_category(
# Remove the association # Remove the association
event_gift_category_crud.remove(db, event_id=event_id, category_id=category_id) event_gift_category_crud.remove(db, event_id=event_id, category_id=category_id)
# Return the category with event-specific properties set to None # Create a new category response with the calculated values
category_copy.display_order = None category_data = {
category_copy.is_visible = None **category.__dict__,
category_copy.total_gifts = None "display_order": display_order,
category_copy.available_gifts = None "is_visible": is_visible,
"total_gifts": total_gifts,
"available_gifts": available_gifts
}
return category_copy # Remove SQLAlchemy state attributes
if "_sa_instance_state" in category_data:
del category_data["_sa_instance_state"]
# Create a new category instance with the enhanced data
category_response = GiftCategory(**category_data)
return category_response
elif force: elif force:
# Check if the user has permission to delete the category # Check if the user has permission to delete the category
# This is a more restrictive operation, so we might want to add additional checks # This is a more restrictive operation, so we might want to add additional checks
@@ -351,7 +430,7 @@ def delete_gift_category(
# If the user doesn't have permission for any of the events, deny the operation # If the user doesn't have permission for any of the events, deny the operation
if event.created_by != current_user.id: if event.created_by != current_user.id:
raise HTTPException( raise HTTPException(
status_code=403, status_code=403,
detail="Not enough permissions. Category is used by events you don't manage." detail="Not enough permissions. Category is used by events you don't manage."
) )
@@ -360,11 +439,29 @@ def delete_gift_category(
event_gift_category_crud.remove(db, event_id=assoc.event_id, category_id=category_id) event_gift_category_crud.remove(db, event_id=assoc.event_id, category_id=category_id)
# Now delete the category itself # Now delete the category itself
return gift_category_crud.remove(db, id=category_id) deleted_category = gift_category_crud.remove(db, id=category_id)
# Create a new category response with the calculated values
category_data = {
**deleted_category.__dict__,
"display_order": display_order,
"is_visible": is_visible,
"total_gifts": total_gifts,
"available_gifts": available_gifts
}
# Remove SQLAlchemy state attributes
if "_sa_instance_state" in category_data:
del category_data["_sa_instance_state"]
# Create a new category instance with the enhanced data
category_response = GiftCategory(**category_data)
return category_response
else: else:
# If no event_id and not force, raise an error # If no event_id and not force, raise an error
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="Must provide event_id to remove association or set force=True to delete category completely" detail="Must provide event_id to remove association or set force=True to delete category completely"
) )
@@ -583,8 +680,9 @@ def create_gift_item(
if not category: if not category:
raise HTTPException(status_code=404, detail="Gift category not found") raise HTTPException(status_code=404, detail="Gift category not found")
# Check category belongs to the same event # Check category belongs to the same event by checking the association
if category.event_id != item_in.event_id: association = event_gift_category_crud.get(db, event_id=item_in.event_id, category_id=item_in.category_id)
if not association:
raise HTTPException(status_code=400, detail="Category does not belong to this event") raise HTTPException(status_code=400, detail="Category does not belong to this event")
return gift_item_crud.create(db, obj_in=item_in) return gift_item_crud.create(db, obj_in=item_in)
@@ -619,8 +717,9 @@ def read_gift_items(
if not category: if not category:
raise HTTPException(status_code=404, detail="Gift category not found") raise HTTPException(status_code=404, detail="Gift category not found")
# Check category belongs to the requested event # Check category belongs to the requested event by checking the association
if category.event_id != event_id: association = event_gift_category_crud.get(db, event_id=event_id, category_id=category_id)
if not association:
raise HTTPException(status_code=400, detail="Category does not belong to this event") raise HTTPException(status_code=400, detail="Category does not belong to this event")
return gift_item_crud.get_multi_by_event( return gift_item_crud.get_multi_by_event(
@@ -688,8 +787,9 @@ def update_gift_item(
if not category: if not category:
raise HTTPException(status_code=404, detail="Gift category not found") raise HTTPException(status_code=404, detail="Gift category not found")
# Check category belongs to the same event # Check category belongs to the same event by checking the association
if category.event_id != gift.event_id: association = event_gift_category_crud.get(db, event_id=gift.event_id, category_id=item_in.category_id)
if not association:
raise HTTPException(status_code=400, detail="Category does not belong to this event") raise HTTPException(status_code=400, detail="Category does not belong to this event")
return gift_item_crud.update(db, db_obj=gift, obj_in=item_in) return gift_item_crud.update(db, db_obj=gift, obj_in=item_in)

View File

@@ -27,7 +27,6 @@ def gift_category_data(mock_event, mock_user):
"description": "Animal-themed toys for Emma", "description": "Animal-themed toys for Emma",
"icon": "toy-icon", "icon": "toy-icon",
"color": "#FF5733", "color": "#FF5733",
"event_id": str(mock_event.id),
"created_by": str(mock_user.id) "created_by": str(mock_user.id)
} }
@@ -130,13 +129,21 @@ class TestGiftsRouter:
assert any(gift["event_id"] == str(mock_event.id) for gift in response.json()) assert any(gift["event_id"] == str(mock_event.id) for gift in response.json())
# Gift Category Tests # Gift Category Tests
def test_create_gift_category_success(self, gift_category_data): def test_create_gift_category_success(self, gift_category_data, mock_event):
response = self.client.post(f"{self.endpoint}/categories/", json=gift_category_data) # Need to provide event_id as a query parameter
response = self.client.post(
f"{self.endpoint}/categories/",
json=gift_category_data,
params={"event_id": str(mock_event.id)}
)
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["name"] == gift_category_data["name"] assert data["name"] == gift_category_data["name"]
assert data["icon"] == gift_category_data["icon"] assert data["icon"] == gift_category_data["icon"]
assert data["color"] == gift_category_data["color"] assert data["color"] == gift_category_data["color"]
# Check that display_order and is_visible are set with default values
assert data["display_order"] == 0
assert data["is_visible"] == True
def test_read_gift_category_success(self): def test_read_gift_category_success(self):
response = self.client.get(f"{self.endpoint}/categories/{self.gift_category.id}") response = self.client.get(f"{self.endpoint}/categories/{self.gift_category.id}")
@@ -174,21 +181,33 @@ class TestGiftsRouter:
assert response.json()["color"] == "#00FF00" assert response.json()["color"] == "#00FF00"
def test_delete_gift_category_success(self): def test_delete_gift_category_success(self):
response = self.client.delete(f"{self.endpoint}/categories/{self.gift_category.id}") response = self.client.delete(
f"{self.endpoint}/categories/{self.gift_category.id}",
params={"force": True}
)
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["id"] == str(self.gift_category.id) assert response.json()["id"] == str(self.gift_category.id)
def test_get_categories_by_event_success(self, mock_event, gift_category_data): def test_get_categories_by_event_success(self, mock_event, gift_category_data):
# Create a new category associated with the mock event # Create a new category associated with the mock event
gift_category_data["event_id"] = str(mock_event.id) self.client.post(
self.client.post(f"{self.endpoint}/categories/", json=gift_category_data) f"{self.endpoint}/categories/",
json=gift_category_data,
params={"event_id": str(mock_event.id)}
)
# Get categories for the event # Get categories for the event
response = self.client.get(f"{self.endpoint}/categories/event/{mock_event.id}") response = self.client.get(f"{self.endpoint}/categories/event/{mock_event.id}")
assert response.status_code == 200 assert response.status_code == 200
assert isinstance(response.json(), list) assert isinstance(response.json(), list)
assert len(response.json()) >= 1 assert len(response.json()) >= 1
assert any(cat["event_id"] == str(mock_event.id) for cat in response.json())
# Check that at least one category has display_order and is_visible set
# These properties come from the EventGiftCategory association
assert any(
cat.get("display_order") is not None and cat.get("is_visible") is not None
for cat in response.json()
)
# Gift Purchase Tests # Gift Purchase Tests
def test_create_gift_purchase_success(self, gift_purchase_data): def test_create_gift_purchase_success(self, gift_purchase_data):
@@ -268,4 +287,4 @@ class TestGiftsRouter:
params=reservation_data params=reservation_data
) )
assert response.status_code == 400 assert response.status_code == 400
assert response.json()["detail"] == "Gift is not available for reservation" assert response.json()["detail"] == "Gift is not available for reservation"

View File

@@ -25,7 +25,7 @@ pytest_plugins = ["pytest_asyncio"]
def db_session(): def db_session():
""" """
Creates a fresh SQLite in-memory database for each test function. Creates a fresh SQLite in-memory database for each test function.
Yields a SQLAlchemy session that can be used for testing. Yields a SQLAlchemy session that can be used for testing.
""" """
# Set up the database # Set up the database
@@ -340,17 +340,27 @@ def gift_category_fixture(db_session, mock_user, mock_event):
""" """
Fixture to create and return a GiftCategory instance. Fixture to create and return a GiftCategory instance.
""" """
# Create the category without event_id and display_order
gift_category = GiftCategory( gift_category = GiftCategory(
id=uuid.uuid4(), id=uuid.uuid4(),
name="Electronics", name="Electronics",
description="Category for electronic gifts", description="Category for electronic gifts",
event_id=mock_event.id, created_by=mock_user.id
created_by=mock_user.id,
display_order=0,
is_visible=True
) )
db_session.add(gift_category) db_session.add(gift_category)
db_session.commit() db_session.commit()
# Create the association between the category and the event
from app.models.gift import EventGiftCategory
event_gift_category = EventGiftCategory(
event_id=mock_event.id,
category_id=gift_category.id,
display_order=0,
is_visible=True
)
db_session.add(event_gift_category)
db_session.commit()
return gift_category return gift_category