feat: Implement bulk delete functionality for photos in API and frontend

This commit introduces a new feature for bulk deleting photos, allowing admins to permanently remove multiple photos at once. The backend has been updated with a new API endpoint for handling bulk delete requests, including response handling for missing photo IDs. The frontend has been enhanced with a confirmation dialog and a button to trigger the bulk delete action, improving the user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09
2025-11-25 13:21:16 -05:00
parent f9e8c476bc
commit a0cc3a985a
4 changed files with 132 additions and 0 deletions
+57
View File
@@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
from src.web.db.session import get_db
from src.web.api.auth import get_current_user
from src.web.api.users import get_current_admin_user
# Redis connection for RQ
redis_conn = Redis(host="localhost", port=6379, db=0, decode_responses=False)
@@ -24,6 +25,8 @@ from src.web.schemas.photos import (
PhotoResponse,
BulkAddFavoritesRequest,
BulkAddFavoritesResponse,
BulkDeletePhotosRequest,
BulkDeletePhotosResponse,
BulkRemoveFavoritesRequest,
BulkRemoveFavoritesResponse,
)
@@ -728,6 +731,60 @@ def bulk_remove_favorites(
)
@router.post("/bulk-delete", response_model=BulkDeletePhotosResponse)
def bulk_delete_photos(
request: BulkDeletePhotosRequest,
current_admin: Annotated[dict, Depends(get_current_admin_user)],
db: Session = Depends(get_db),
) -> BulkDeletePhotosResponse:
"""Delete multiple photos and all related data (faces, encodings, tags, favorites)."""
from src.web.db.models import Photo, PhotoTagLinkage
photo_ids = list(dict.fromkeys(request.photo_ids))
if not photo_ids:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="photo_ids list cannot be empty",
)
try:
photos = db.query(Photo).filter(Photo.id.in_(photo_ids)).all()
found_ids = {photo.id for photo in photos}
missing_ids = sorted(set(photo_ids) - found_ids)
deleted_count = 0
for photo in photos:
# Remove tag linkages explicitly (in addition to cascade) to keep counts accurate
db.query(PhotoTagLinkage).filter(
PhotoTagLinkage.photo_id == photo.id
).delete(synchronize_session=False)
db.delete(photo)
deleted_count += 1
db.commit()
except HTTPException:
db.rollback()
raise
except Exception as exc: # pragma: no cover - safety net
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete photos: {exc}",
)
admin_username = current_admin.get("username", "unknown")
message_parts = [f"Deleted {deleted_count} photo(s)"]
if missing_ids:
message_parts.append(f"{len(missing_ids)} photo(s) not found")
message_parts.append(f"Request by admin: {admin_username}")
return BulkDeletePhotosResponse(
message="; ".join(message_parts),
deleted_count=deleted_count,
missing_photo_ids=missing_ids,
)
@router.post("/{photo_id}/open-folder")
def open_photo_folder(photo_id: int, db: Session = Depends(get_db)) -> dict:
"""Open the folder containing the photo in the system file manager and select the file.
+17
View File
@@ -74,3 +74,20 @@ class BulkRemoveFavoritesResponse(BaseModel):
not_favorite_count: int
total_requested: int
class BulkDeletePhotosRequest(BaseModel):
"""Request to delete multiple photos permanently."""
photo_ids: List[int] = Field(..., description="List of photo IDs to delete")
class BulkDeletePhotosResponse(BaseModel):
"""Response for bulk delete photos operation."""
message: str
deleted_count: int
missing_photo_ids: List[int] = Field(
default_factory=list,
description="Photo IDs that were requested but not found",
)