phase 10
All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 7s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 7s
CI/CD Pipeline / VM Test - performance (push) Successful in 7s
CI/CD Pipeline / VM Test - security (push) Successful in 7s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Successful in 15s
CI/CD Pipeline / Nix Flake Check (push) Successful in 41s
CI/CD Pipeline / CI Summary (push) Successful in 1s

This commit is contained in:
Danilo Reyes
2025-11-02 14:26:15 -06:00
parent ce0b692aee
commit 3eb3d977f9
18 changed files with 3079 additions and 32 deletions

View File

@@ -15,6 +15,8 @@ from app.images.schemas import (
BoardImageCreate,
BoardImageResponse,
BoardImageUpdate,
BulkImageUpdate,
BulkUpdateResponse,
ImageListResponse,
ImageResponse,
ImageUploadResponse,
@@ -357,6 +359,83 @@ async def remove_image_from_board(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not on this board")
@router.patch("/boards/{board_id}/images/bulk", response_model=BulkUpdateResponse)
async def bulk_update_board_images(
board_id: UUID,
data: BulkImageUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
Bulk update multiple images on a board.
Applies the same changes to all specified images. Useful for multi-selection operations.
"""
# Verify board ownership
from sqlalchemy import select
board_result = await db.execute(select(Board).where(Board.id == board_id))
board = board_result.scalar_one_or_none()
if not board:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found")
if board.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
# Update each image
repo = ImageRepository(db)
updated_ids = []
failed_count = 0
for image_id in data.image_ids:
try:
# Calculate new position if delta provided
position = None
if data.position_delta:
# Get current position
board_image = await repo.get_board_image(board_id, image_id)
if board_image and board_image.position:
current_pos = board_image.position
position = {
"x": current_pos.get("x", 0) + data.position_delta["dx"],
"y": current_pos.get("y", 0) + data.position_delta["dy"],
}
# Calculate new z-order if delta provided
z_order = None
if data.z_order_delta is not None:
board_image = await repo.get_board_image(board_id, image_id)
if board_image:
z_order = board_image.z_order + data.z_order_delta
# Update the image
updated = await repo.update_board_image(
board_id=board_id,
image_id=image_id,
position=position,
transformations=data.transformations,
z_order=z_order,
group_id=None, # Bulk operations don't change groups
)
if updated:
updated_ids.append(image_id)
else:
failed_count += 1
except Exception as e:
print(f"Error updating image {image_id}: {e}")
failed_count += 1
continue
return BulkUpdateResponse(
updated_count=len(updated_ids),
failed_count=failed_count,
image_ids=updated_ids,
)
@router.get("/boards/{board_id}/images", response_model=list[BoardImageResponse])
async def get_board_images(
board_id: UUID,

View File

@@ -120,6 +120,31 @@ class BoardImageResponse(BaseModel):
from_attributes = True
class BulkImageUpdate(BaseModel):
"""Schema for bulk updating multiple images."""
image_ids: list[UUID] = Field(..., description="List of image IDs to update")
position_delta: dict[str, float] | None = Field(None, description="Position delta to apply")
transformations: dict[str, Any] | None = Field(None, description="Transformations to apply")
z_order_delta: int | None = Field(None, description="Z-order delta to apply")
@field_validator("position_delta")
@classmethod
def validate_position_delta(cls, v: dict[str, float] | None) -> dict[str, float] | None:
"""Validate position delta has dx and dy."""
if v is not None and ("dx" not in v or "dy" not in v):
raise ValueError("Position delta must contain 'dx' and 'dy'")
return v
class BulkUpdateResponse(BaseModel):
"""Response for bulk update operation."""
updated_count: int = Field(..., description="Number of images updated")
failed_count: int = Field(default=0, description="Number of images that failed to update")
image_ids: list[UUID] = Field(..., description="IDs of successfully updated images")
class ImageListResponse(BaseModel):
"""Paginated list of images."""