Big refactor of gift categories model
This commit is contained in:
@@ -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 ###
|
||||
@@ -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)
|
||||
@@ -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(
|
||||
*,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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}>"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user