Big refactor of gift categories model
All checks were successful
Build and Push Docker Images / changes (push) Successful in 5s
Build and Push Docker Images / build-backend (push) Successful in 52s
Build and Push Docker Images / build-frontend (push) Has been skipped

This commit is contained in:
2025-03-16 14:51:04 +01:00
parent ed017a42ed
commit 4ef202cc5a
7 changed files with 643 additions and 97 deletions

View File

@@ -0,0 +1,87 @@
"""refactor_gift_categories_to_support_multiple_events
Revision ID: 60c6bfef5416
Revises: 38bf9e7e74b3
Create Date: 2025-03-16 14:48:08.137051
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy import text
from datetime import datetime
# revision identifiers, used by Alembic.
revision: str = '60c6bfef5416'
down_revision: Union[str, None] = '38bf9e7e74b3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('event_gift_categories',
sa.Column('event_id', sa.UUID(), nullable=False),
sa.Column('category_id', sa.UUID(), nullable=False),
sa.Column('display_order', sa.Integer(), nullable=True),
sa.Column('is_visible', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['category_id'], ['gift_categories.id'], ),
sa.ForeignKeyConstraint(['event_id'], ['events.id'], ),
sa.PrimaryKeyConstraint('event_id', 'category_id')
)
# Migrate existing data from gift_categories to event_gift_categories
# Get connection
connection = op.get_bind()
# Get all existing gift categories
categories = connection.execute(
text("SELECT id, event_id, display_order, is_visible FROM gift_categories")
).fetchall()
# Insert data into the new association table
for category in categories:
category_id = category[0]
event_id = category[1]
display_order = category[2] if category[2] is not None else 0
is_visible = category[3] if category[3] is not None else True
now = datetime.now().isoformat()
connection.execute(
text(f"""
INSERT INTO event_gift_categories
(event_id, category_id, display_order, is_visible, created_at)
VALUES
('{event_id}', '{category_id}', {display_order}, {is_visible}, '{now}')
""")
)
# Now we can safely modify the gift_categories table
op.alter_column('event_themes', 'asset_image_urls',
existing_type=postgresql.JSON(astext_type=sa.Text()),
nullable=True)
op.drop_constraint('uq_event_category_name', 'gift_categories', type_='unique')
op.create_unique_constraint(None, 'gift_categories', ['name'])
op.drop_constraint('gift_categories_event_id_fkey', 'gift_categories', type_='foreignkey')
op.drop_column('gift_categories', 'display_order')
op.drop_column('gift_categories', 'event_id')
op.drop_column('gift_categories', 'is_visible')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('gift_categories', sa.Column('is_visible', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('gift_categories', sa.Column('event_id', sa.UUID(), autoincrement=False, nullable=False))
op.add_column('gift_categories', sa.Column('display_order', sa.INTEGER(), autoincrement=False, nullable=True))
op.create_foreign_key('gift_categories_event_id_fkey', 'gift_categories', 'events', ['event_id'], ['id'])
op.drop_constraint(None, 'gift_categories', type_='unique')
op.create_unique_constraint('uq_event_category_name', 'gift_categories', ['event_id', 'name'])
op.alter_column('event_themes', 'asset_image_urls',
existing_type=postgresql.JSON(astext_type=sa.Text()),
nullable=False)
op.drop_table('event_gift_categories')
# ### end Alembic commands ###

View File

@@ -1,19 +1,20 @@
from typing import List, Optional, Dict, Any
from typing import List, Optional, Dict, Any, Union
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from sqlalchemy.orm import Session
from app.api.dependencies.auth import get_current_active_user, get_current_user
from app.crud.gift import gift_item_crud, gift_category_crud, gift_purchase_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.event import event_crud
from app.models.gift import GiftStatus, GiftPriority
from app.models.gift import GiftStatus, GiftPriority, EventGiftCategory, GiftItem
from app.models.user import User
from app.schemas.gifts import (
GiftItem, GiftItemCreate, GiftItemUpdate,
GiftCategory, GiftCategoryCreate, GiftCategoryUpdate,
GiftPurchase, GiftPurchaseCreate, GiftPurchaseUpdate
GiftPurchase, GiftPurchaseCreate, GiftPurchaseUpdate,
EventGiftCategoryCreate, EventGiftCategoryUpdate, EventGiftCategoryInDB
)
from app.core.database import get_db
@@ -26,13 +27,14 @@ def create_gift_category(
*,
db: Session = Depends(get_db),
category_in: GiftCategoryCreate,
event_id: UUID,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Create new gift category.
Create new gift category and associate it with an event.
"""
# Check if user has permission to manage this event
event = event_crud.get(db, category_in.event_id)
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
@@ -41,7 +43,23 @@ def create_gift_category(
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_category_crud.create(db, obj_in=category_in)
# Create the category
category = gift_category_crud.create(db, obj_in=category_in)
# Create the association between the category and the event
association_data = EventGiftCategoryCreate(
event_id=event_id,
category_id=category.id,
display_order=0, # Default display order
is_visible=True # Default visibility
)
event_gift_category_crud.create(db, obj_in=association_data)
# Set display properties for the response
category.display_order = 0
category.is_visible = True
return category
@router.get("/categories/event/{event_id}", response_model=List[GiftCategory])
@@ -67,11 +85,34 @@ def read_gift_categories(
if not current_user and not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Get categories
categories = gift_category_crud.get_multi_by_event(
# Get categories for this event using the association table
categories = event_gift_category_crud.get_categories_by_event(
db, event_id=event_id, skip=skip, limit=limit, include_hidden=include_hidden
)
# Enhance categories with display information from the association
for category in categories:
# Get the association to access display_order and is_visible
association = event_gift_category_crud.get(
db, event_id=event_id, category_id=category.id
)
if association:
category.display_order = association.display_order
category.is_visible = association.is_visible
# Calculate statistics for this event
total_gifts = 0
available_gifts = 0
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
category.available_gifts = available_gifts
# If include_gifts is true, fetch gift items for each category
if include_gifts:
for category in categories:
@@ -89,31 +130,79 @@ def read_gift_category(
*,
db: Session = Depends(get_db),
category_id: UUID = Path(...),
event_id: Optional[UUID] = None,
include_gifts: bool = False,
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Get gift category by ID.
Get gift category by ID. If event_id is provided, includes event-specific display settings.
"""
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if event is public or user is authorized
event = event_crud.get(db, category.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Initialize event-specific properties
category.display_order = None
category.is_visible = None
category.total_gifts = None
category.available_gifts = None
if not event.is_public and not current_user:
raise HTTPException(status_code=403, detail="Not enough permissions")
# If event_id is provided, get event-specific information
if event_id:
# Check if event exists and is accessible
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# 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=category.event_id, category_id=category.id,
include_hidden=current_user is not None # Only include hidden for logged-in users
)
# Set the gifts attribute which is initially None in the model
# For public access, ensure event is public
if not event.is_public and not current_user:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Check if this category is associated with the event
association = event_gift_category_crud.get(db, event_id=event_id, category_id=category_id)
if not association:
raise HTTPException(status_code=404, detail="Category not associated with this event")
# Set event-specific display properties
category.display_order = association.display_order
category.is_visible = association.is_visible
# Calculate statistics for this event
total_gifts = 0
available_gifts = 0
# If include_gifts is true, fetch gift items for the category in this event
if include_gifts:
gifts = gift_item_crud.get_multi_by_event(
db, event_id=event_id, category_id=category.id,
include_hidden=current_user is not None # Only include hidden for logged-in users
)
# Set the gifts attribute
setattr(category, "gifts", gifts)
# Calculate statistics
for gift in gifts:
total_gifts += 1
if gift.status == GiftStatus.AVAILABLE and gift.is_visible:
available_gifts += 1
else:
# Calculate statistics without fetching all gifts
gifts_query = db.query(GiftItem).filter(
GiftItem.event_id == event_id,
GiftItem.category_id == category_id
)
total_gifts = gifts_query.count()
available_gifts = gifts_query.filter(
GiftItem.status == GiftStatus.AVAILABLE,
GiftItem.is_visible == True
).count()
category.total_gifts = total_gifts
category.available_gifts = available_gifts
elif include_gifts:
# 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
gifts = db.query(GiftItem).filter(GiftItem.category_id == category_id).all()
setattr(category, "gifts", gifts)
return category
@@ -125,25 +214,76 @@ def update_gift_category(
db: Session = Depends(get_db),
category_id: UUID = Path(...),
category_in: GiftCategoryUpdate,
event_id: Optional[UUID] = None,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Update a gift category.
Update a gift category. If event_id is provided, also updates the event-specific settings.
"""
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if user has permission to manage this event
event = event_crud.get(db, category.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Update the category itself
updated_category = gift_category_crud.update(db, db_obj=category, obj_in=category_in)
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Initialize event-specific properties for the response
updated_category.display_order = None
updated_category.is_visible = None
updated_category.total_gifts = None
updated_category.available_gifts = None
return gift_category_crud.update(db, db_obj=category, obj_in=category_in)
# If event_id is provided, update the event-specific settings
if event_id:
# Check if event exists
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Check if this category is associated with the event
association = event_gift_category_crud.get(db, event_id=event_id, category_id=category_id)
if not association:
# If not associated, create the association
association_data = EventGiftCategoryCreate(
event_id=event_id,
category_id=category_id,
display_order=0, # Default display order
is_visible=True # Default visibility
)
association = event_gift_category_crud.create(db, obj_in=association_data)
else:
# If display_order or is_visible are in the update data, update the association
association_update = {}
if hasattr(category_in, 'display_order') and category_in.display_order is not None:
association_update['display_order'] = category_in.display_order
if hasattr(category_in, 'is_visible') and category_in.is_visible is not None:
association_update['is_visible'] = category_in.is_visible
if association_update:
association = event_gift_category_crud.update(
db, db_obj=association, obj_in=association_update
)
# Set event-specific properties for the response
updated_category.display_order = association.display_order
updated_category.is_visible = association.is_visible
# Calculate statistics for this event
gifts_query = db.query(GiftItem).filter(
GiftItem.event_id == event_id,
GiftItem.category_id == category_id
)
updated_category.total_gifts = gifts_query.count()
updated_category.available_gifts = gifts_query.filter(
GiftItem.status == GiftStatus.AVAILABLE,
GiftItem.is_visible == True
).count()
return updated_category
@router.delete("/categories/{category_id}", response_model=GiftCategory)
@@ -151,25 +291,242 @@ def delete_gift_category(
*,
db: Session = Depends(get_db),
category_id: UUID = Path(...),
event_id: Optional[UUID] = None,
force: bool = False,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Delete a gift category.
Delete a gift category. If event_id is provided, only removes the association with that event.
If force=True and no event_id is provided, deletes the category completely.
"""
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if user has permission to manage this event
event = event_crud.get(db, category.event_id)
# Make a copy of the category for the response
category_copy = GiftCategory.model_validate(category)
if event_id:
# Check if event exists
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Check if this category is associated with the event
association = event_gift_category_crud.get(db, event_id=event_id, category_id=category_id)
if not association:
raise HTTPException(status_code=404, detail="Category not associated with this event")
# Remove the association
event_gift_category_crud.remove(db, event_id=event_id, category_id=category_id)
# Return the category with event-specific properties set to None
category_copy.display_order = None
category_copy.is_visible = None
category_copy.total_gifts = None
category_copy.available_gifts = None
return category_copy
elif force:
# Check if the user has permission to delete the category
# This is a more restrictive operation, so we might want to add additional checks
# Check if the category is used by any events
# Get all associations for this category
associations = db.query(EventGiftCategory).filter(
EventGiftCategory.category_id == category_id
).all()
if associations and len(associations) > 0:
# If there are associations, we need to check if the user has permission to manage all events
for assoc in associations:
event = event_crud.get(db, assoc.event_id)
if not event:
continue
# If the user doesn't have permission for any of the events, deny the operation
if event.created_by != current_user.id:
raise HTTPException(
status_code=403,
detail="Not enough permissions. Category is used by events you don't manage."
)
# Remove all associations
for assoc in associations:
event_gift_category_crud.remove(db, event_id=assoc.event_id, category_id=category_id)
# Now delete the category itself
return gift_category_crud.remove(db, id=category_id)
else:
# If no event_id and not force, raise an error
raise HTTPException(
status_code=400,
detail="Must provide event_id to remove association or set force=True to delete category completely"
)
# ===== EVENT-CATEGORY ASSOCIATIONS ===== #
@router.post("/events/{event_id}/categories/{category_id}", response_model=GiftCategory)
def associate_category_with_event(
*,
db: Session = Depends(get_db),
event_id: UUID = Path(...),
category_id: UUID = Path(...),
display_order: int = 0,
is_visible: bool = True,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Associate an existing category with an event.
"""
# Check if event exists
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
# Check permissions
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_category_crud.remove(db, id=category_id)
# Check if category exists
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if association already exists
existing_association = event_gift_category_crud.get(db, event_id=event_id, category_id=category_id)
if existing_association:
raise HTTPException(status_code=400, detail="Category already associated with this event")
# Create the association
association_data = EventGiftCategoryCreate(
event_id=event_id,
category_id=category_id,
display_order=display_order,
is_visible=is_visible
)
event_gift_category_crud.create(db, obj_in=association_data)
# Set display properties for the response
category.display_order = display_order
category.is_visible = is_visible
# Calculate statistics for this event
gifts_query = db.query(GiftItem).filter(
GiftItem.event_id == event_id,
GiftItem.category_id == category_id
)
category.total_gifts = gifts_query.count()
category.available_gifts = gifts_query.filter(
GiftItem.status == GiftStatus.AVAILABLE,
GiftItem.is_visible == True
).count()
return category
@router.put("/events/{event_id}/categories/{category_id}", response_model=GiftCategory)
def update_category_event_settings(
*,
db: Session = Depends(get_db),
event_id: UUID = Path(...),
category_id: UUID = Path(...),
display_order: Optional[int] = None,
is_visible: Optional[bool] = None,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Update the display settings for a category in an event.
"""
# Check if event exists
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Check if category exists
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if association exists
association = event_gift_category_crud.get(db, event_id=event_id, category_id=category_id)
if not association:
raise HTTPException(status_code=404, detail="Category not associated with this event")
# Update the association
update_data = {}
if display_order is not None:
update_data['display_order'] = display_order
if is_visible is not None:
update_data['is_visible'] = is_visible
if update_data:
association = event_gift_category_crud.update(db, db_obj=association, obj_in=update_data)
# Set display properties for the response
category.display_order = association.display_order
category.is_visible = association.is_visible
# Calculate statistics for this event
gifts_query = db.query(GiftItem).filter(
GiftItem.event_id == event_id,
GiftItem.category_id == category_id
)
category.total_gifts = gifts_query.count()
category.available_gifts = gifts_query.filter(
GiftItem.status == GiftStatus.AVAILABLE,
GiftItem.is_visible == True
).count()
return category
@router.get("/categories/{category_id}/events", response_model=List[Dict[str, Any]])
def get_events_for_category(
*,
db: Session = Depends(get_db),
category_id: UUID = Path(...),
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Get all events that use a specific category.
"""
# Check if category exists
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Get all events for this category
events = event_gift_category_crud.get_events_by_category(db, category_id=category_id)
# Filter events that the user has permission to see
result = []
for event in events:
# Check if user has permission to see this event
if event.created_by == current_user.id or event.is_public:
# Get the association to access display_order and is_visible
association = event_gift_category_crud.get(db, event_id=event.id, category_id=category_id)
if association:
# Create a response with event details and association settings
event_data = {
"event_id": event.id,
"event_title": event.title,
"event_date": event.event_date,
"display_order": association.display_order,
"is_visible": association.is_visible
}
result.append(event_data)
return result
@router.put("/categories/{category_id}/reorder", response_model=GiftCategory)
@@ -404,7 +761,6 @@ def reserve_gift_item(
Reserve a gift item for a guest.
"""
gift = gift_item_crud.get(db, id=item_id)
print(f"Gift {gift}")
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
@@ -581,12 +937,3 @@ def read_gift_purchases_by_guest(
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_purchase_crud.get_by_guest(db, guest_id=guest_id)
# For public users, additional validation
# Check if event is public
event = event_crud.get(db, gift.event_id)
if not event or not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_item_crud.cancel_reservation(db, gift_id=item_id, guest_id=guest_id)

View File

@@ -23,11 +23,13 @@ from app.schemas.events import (
from app.api.routes.events import guests
from app.api.routes.events import rsvps
from app.api.routes.events import gifts
logger = logging.getLogger(__name__)
events_router = APIRouter()
events_router.include_router(guests.router, prefix="/guests", tags=["guests"])
events_router.include_router(rsvps.router, prefix="/rsvps", tags=["rsvps"])
events_router.include_router(gifts.router, prefix="/gifts", tags=["gifts"])
def validate_event_access(
*,

View File

@@ -6,12 +6,16 @@ from sqlalchemy import asc, desc
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.gift import GiftItem, GiftCategory, GiftPurchase, GiftStatus
from app.models import gift as gift_models
from app.models.gift import GiftItem, GiftCategory, GiftPurchase, GiftStatus, EventGiftCategory
from app.models.guest import Guest
from app.models.event import Event
from app.schemas import gifts as gift_schemas
from app.schemas.gifts import (
GiftItemCreate, GiftItemUpdate,
GiftCategoryCreate, GiftCategoryUpdate,
GiftPurchaseCreate, GiftPurchaseUpdate
GiftPurchaseCreate, GiftPurchaseUpdate,
EventGiftCategoryCreate, EventGiftCategoryUpdate
)
@@ -130,13 +134,110 @@ class CRUDGiftItem(CRUDBase[GiftItem, GiftItemCreate, GiftItemUpdate]):
return gift
class CRUDEventGiftCategory:
def create(self, db: Session, *, obj_in: EventGiftCategoryCreate) -> EventGiftCategory:
"""Create a new event-category association"""
obj_data = obj_in.model_dump(exclude_unset=True)
# Ensure UUID fields are properly converted if they're strings
for field in ["event_id", "category_id"]:
if field in obj_data and obj_data[field] is not None:
if isinstance(obj_data[field], str):
obj_data[field] = UUID(obj_data[field])
db_obj = EventGiftCategory(**obj_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def get(self, db: Session, *, event_id: UUID, category_id: UUID) -> Optional[EventGiftCategory]:
"""Get a specific event-category association"""
return db.query(EventGiftCategory).filter(
EventGiftCategory.event_id == event_id,
EventGiftCategory.category_id == category_id
).first()
def update(
self, db: Session, *, db_obj: EventGiftCategory, obj_in: Union[EventGiftCategoryUpdate, Dict[str, Any]]
) -> EventGiftCategory:
"""Update an event-category association"""
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.model_dump(exclude_unset=True)
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(self, db: Session, *, event_id: UUID, category_id: UUID) -> Optional[EventGiftCategory]:
"""Remove an event-category association"""
obj = self.get(db, event_id=event_id, category_id=category_id)
if obj:
db.delete(obj)
db.commit()
return obj
def get_categories_by_event(
self, db: Session, *, event_id: UUID, skip: int = 0, limit: int = 100,
include_hidden: bool = False
) -> List[GiftCategory]:
"""Get categories for a specific event with filtering options"""
query = db.query(GiftCategory).join(
EventGiftCategory,
GiftCategory.id == EventGiftCategory.category_id
).filter(EventGiftCategory.event_id == event_id)
if not include_hidden:
query = query.filter(EventGiftCategory.is_visible == True)
# Order by display_order then name
query = query.order_by(
asc(EventGiftCategory.display_order),
asc(GiftCategory.name)
)
return query.offset(skip).limit(limit).all()
def get_events_by_category(
self, db: Session, *, category_id: UUID, skip: int = 0, limit: int = 100
) -> List[Event]:
"""Get events for a specific category"""
query = db.query(Event).join(
EventGiftCategory,
Event.id == EventGiftCategory.event_id
).filter(EventGiftCategory.category_id == category_id)
return query.offset(skip).limit(limit).all()
def reorder_categories(
self, db: Session, *, event_id: UUID, category_orders: Dict[UUID, int]
) -> List[EventGiftCategory]:
"""Update display order of multiple categories for an event"""
associations = db.query(EventGiftCategory).filter(
EventGiftCategory.event_id == event_id
).all()
for assoc in associations:
if assoc.category_id in category_orders:
assoc.display_order = category_orders[assoc.category_id]
db.commit()
return associations
class CRUDGiftCategory(CRUDBase[GiftCategory, GiftCategoryCreate, GiftCategoryUpdate]):
def create(self, db: Session, *, obj_in: GiftCategoryCreate) -> GiftCategory:
"""Create a new gift category with appropriate type conversions"""
obj_data = obj_in.model_dump(exclude_unset=True)
# Ensure UUID fields are properly converted if they're strings
for field in ["event_id", "created_by"]:
for field in ["created_by"]:
if field in obj_data and obj_data[field] is not None:
if isinstance(obj_data[field], str):
obj_data[field] = UUID(obj_data[field])
@@ -147,34 +248,6 @@ class CRUDGiftCategory(CRUDBase[GiftCategory, GiftCategoryCreate, GiftCategoryUp
db.refresh(db_obj)
return db_obj
def get_multi_by_event(
self, db: Session, event_id: UUID, skip: int = 0, limit: int = 100,
include_hidden: bool = False
) -> List[GiftCategory]:
"""Get categories for a specific event with filtering options"""
query = db.query(self.model).filter(GiftCategory.event_id == event_id)
if not include_hidden:
query = query.filter(GiftCategory.is_visible == True)
# Order by display_order then name
query = query.order_by(asc(GiftCategory.display_order), asc(GiftCategory.name))
return query.offset(skip).limit(limit).all()
def reorder_categories(
self, db: Session, *, event_id: UUID, category_orders: Dict[UUID, int]
) -> List[GiftCategory]:
"""Update display order of multiple categories at once"""
categories = self.get_multi_by_event(db, event_id, include_hidden=True)
for category in categories:
if category.id in category_orders:
category.display_order = category_orders[category.id]
db.commit()
return categories
def reorder_gifts(
self, db: Session, *, category_id: UUID, gift_orders: Dict[UUID, int]
) -> GiftCategory:
@@ -241,4 +314,5 @@ class CRUDGiftPurchase(CRUDBase[GiftPurchase, GiftPurchaseCreate, GiftPurchaseUp
# Create CRUD instances
gift_item_crud = CRUDGiftItem(GiftItem)
gift_category_crud = CRUDGiftCategory(GiftCategory)
gift_purchase_crud = CRUDGiftPurchase(GiftPurchase)
gift_purchase_crud = CRUDGiftPurchase(GiftPurchase)
event_gift_category_crud = CRUDEventGiftCategory()

View File

@@ -49,7 +49,7 @@ class Event(Base, UUIDMixin, TimestampMixin):
managers = relationship("EventManager", back_populates="event")
guests = relationship("Guest", back_populates="event")
gifts = relationship("GiftItem", back_populates="event")
gift_categories = relationship("GiftCategory", back_populates="event")
category_associations = relationship("EventGiftCategory", back_populates="event")
media = relationship("EventMedia", back_populates="event")
# updates = relationship("EventUpdate", back_populates="event") # Keep commented out
rsvps = relationship("RSVP", back_populates="event")

View File

@@ -121,38 +121,47 @@ class GiftPurchase(Base, UUIDMixin):
return f"<GiftPurchase gift_id={self.gift_id} guest_id={self.guest_id}>"
class EventGiftCategory(Base):
__tablename__ = 'event_gift_categories'
event_id = Column(UUID(as_uuid=True), ForeignKey('events.id'), primary_key=True)
category_id = Column(UUID(as_uuid=True), ForeignKey('gift_categories.id'), primary_key=True)
display_order = Column(Integer, default=0)
is_visible = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
# Relationships
event = relationship("Event", back_populates="category_associations")
category = relationship("GiftCategory", back_populates="event_associations")
def __repr__(self):
return f"<EventGiftCategory event_id={self.event_id} category_id={self.category_id}>"
class GiftCategory(Base, UUIDMixin, TimestampMixin):
__tablename__ = 'gift_categories'
# Foreign Keys
event_id = Column(UUID(as_uuid=True), ForeignKey('events.id'), nullable=False)
created_by = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False)
# Category Details
name = Column(String, nullable=False)
name = Column(String, nullable=False, unique=True)
description = Column(String)
# Display
icon = Column(String) # Icon identifier or URL
color = Column(String) # Color code (hex/rgb)
display_order = Column(Integer, default=0)
is_visible = Column(Boolean, default=True)
# Additional Settings
custom_fields = Column(JSONB)
# Relationships
event = relationship("Event", back_populates="gift_categories")
event_associations = relationship("EventGiftCategory", back_populates="category")
gifts = relationship("GiftItem",
back_populates="category",
order_by="GiftItem.display_order")
created_by_user = relationship("User", foreign_keys=[created_by])
# Ensure unique category names within an event
__table_args__ = (
UniqueConstraint('event_id', 'name', name='uq_event_category_name'),
)
def __repr__(self):
return f"<GiftCategory {self.name}>"

View File

@@ -123,7 +123,6 @@ class GiftCategoryBase(BaseModel):
# Schema for creating a category
class GiftCategoryCreate(GiftCategoryBase):
event_id: UUID
created_by: UUID
@@ -133,15 +132,12 @@ class GiftCategoryUpdate(BaseModel):
description: Optional[str] = None
icon: Optional[str] = None
color: Optional[str] = None
display_order: Optional[int] = None
is_visible: Optional[bool] = None
custom_fields: Optional[Dict[str, Any]] = None
# Schema for reading a category
class GiftCategoryInDB(GiftCategoryBase):
id: UUID
event_id: UUID
created_by: UUID
created_at: datetime
updated_at: datetime
@@ -150,11 +146,42 @@ class GiftCategoryInDB(GiftCategoryBase):
from_attributes = True
# Event-Category association schemas
class EventGiftCategoryBase(BaseModel):
event_id: UUID
category_id: UUID
display_order: int = 0
is_visible: bool = True
# Schema for creating an event-category association
class EventGiftCategoryCreate(EventGiftCategoryBase):
pass
# Schema for updating an event-category association
class EventGiftCategoryUpdate(BaseModel):
display_order: Optional[int] = None
is_visible: Optional[bool] = None
# Schema for reading an event-category association
class EventGiftCategoryInDB(EventGiftCategoryBase):
created_at: datetime
class Config:
from_attributes = True
# Public category response with statistics
class GiftCategory(GiftCategoryInDB):
total_gifts: int
available_gifts: int
# These properties will be calculated for a specific event in the API
total_gifts: Optional[int] = None
available_gifts: Optional[int] = None
gifts: Optional[List[GiftItem]] = None
# For display in a specific event context
display_order: Optional[int] = None
is_visible: Optional[bool] = None
class Config:
from_attributes = True
@@ -195,4 +222,4 @@ class GiftPurchaseInDB(GiftPurchaseBase):
# Public gift purchase response
class GiftPurchase(GiftPurchaseInDB):
class Config:
from_attributes = True
from_attributes = True