|
12 | 12 | GalleryUpdate, |
13 | 13 | Photo, |
14 | 14 | PhotoCreate, |
15 | | - Item, |
16 | | - ItemCreate, |
| 15 | + #Item, |
| 16 | + #ItemCreate, |
17 | 17 | Organization, |
18 | 18 | OrganizationCreate, |
19 | 19 | OrganizationUpdate, |
|
22 | 22 | ProjectAccessCreate, |
23 | 23 | ProjectAccessUpdate, |
24 | 24 | ProjectCreate, |
| 25 | + ProjectInvitation, |
25 | 26 | ProjectUpdate, |
26 | 27 | User, |
27 | 28 | UserCreate, |
@@ -68,7 +69,7 @@ def authenticate(*, session: Session, email: str, password: str) -> User | None: |
68 | 69 | return db_user |
69 | 70 |
|
70 | 71 |
|
71 | | -def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item: |
| 72 | +#def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item: |
72 | 73 | db_item = Item.model_validate(item_in, update={"owner_id": owner_id}) |
73 | 74 | session.add(db_item) |
74 | 75 | session.commit() |
@@ -252,6 +253,87 @@ def delete_gallery(*, session: Session, gallery_id: uuid.UUID) -> None: |
252 | 253 | session.commit() |
253 | 254 |
|
254 | 255 |
|
| 256 | +def create_photo(*, session: Session, photo_in: PhotoCreate) -> Photo: |
| 257 | + """Create a new photo record for a gallery and keep gallery.photo_count in sync.""" |
| 258 | + db_obj = Photo.model_validate(photo_in) |
| 259 | + session.add(db_obj) |
| 260 | + session.commit() |
| 261 | + session.refresh(db_obj) |
| 262 | + |
| 263 | + # Update gallery photo_count |
| 264 | + gallery = session.get(Gallery, photo_in.gallery_id) |
| 265 | + if gallery is not None: |
| 266 | + # Recalculate from DB to avoid drift |
| 267 | + gallery.photo_count = count_photos_in_gallery( |
| 268 | + session=session, gallery_id=gallery.id |
| 269 | + ) |
| 270 | + session.add(gallery) |
| 271 | + session.commit() |
| 272 | + session.refresh(gallery) |
| 273 | + |
| 274 | + return db_obj |
| 275 | + |
| 276 | + |
| 277 | +def delete_photos( |
| 278 | + *, session: Session, gallery_id: uuid.UUID, photo_ids: list[uuid.UUID] |
| 279 | +) -> int: |
| 280 | + """Delete photos by ID for a gallery and update gallery.photo_count. |
| 281 | +
|
| 282 | + Returns the number of Photo rows deleted. |
| 283 | + """ |
| 284 | + if not photo_ids: |
| 285 | + return 0 |
| 286 | + |
| 287 | + # Only delete photos that belong to this gallery |
| 288 | + from app.models import Photo # local import to avoid circulars in some tools |
| 289 | + |
| 290 | + statement = select(Photo).where( |
| 291 | + Photo.gallery_id == gallery_id, Photo.id.in_(photo_ids) # type: ignore[arg-type] |
| 292 | + ) |
| 293 | + photos = list(session.exec(statement).all()) |
| 294 | + deleted_count = 0 |
| 295 | + |
| 296 | + for photo in photos: |
| 297 | + session.delete(photo) |
| 298 | + deleted_count += 1 |
| 299 | + |
| 300 | + # Update gallery photo_count after deletions |
| 301 | + gallery = session.get(Gallery, gallery_id) |
| 302 | + if gallery is not None: |
| 303 | + gallery.photo_count = max( |
| 304 | + 0, count_photos_in_gallery(session=session, gallery_id=gallery.id) |
| 305 | + ) |
| 306 | + session.add(gallery) |
| 307 | + |
| 308 | + session.commit() |
| 309 | + |
| 310 | + return deleted_count |
| 311 | + |
| 312 | + |
| 313 | +def get_photos_by_gallery( |
| 314 | + *, session: Session, gallery_id: uuid.UUID, skip: int = 0, limit: int = 100 |
| 315 | +) -> list[Photo]: |
| 316 | + """Get photos belonging to a specific gallery.""" |
| 317 | + statement = ( |
| 318 | + select(Photo) |
| 319 | + .where(Photo.gallery_id == gallery_id) |
| 320 | + .offset(skip) |
| 321 | + .limit(limit) |
| 322 | + .order_by(desc(Photo.created_at)) |
| 323 | + ) |
| 324 | + return list(session.exec(statement).all()) |
| 325 | + |
| 326 | + |
| 327 | +def count_photos_in_gallery(*, session: Session, gallery_id: uuid.UUID) -> int: |
| 328 | + """Count how many photos are in a gallery.""" |
| 329 | + statement = ( |
| 330 | + select(func.count()) |
| 331 | + .select_from(Photo) |
| 332 | + .where(Photo.gallery_id == gallery_id) |
| 333 | + ) |
| 334 | + return session.exec(statement).one() |
| 335 | + |
| 336 | + |
255 | 337 | # ============================================================================ |
256 | 338 | # PROJECT ACCESS CRUD |
257 | 339 | # ============================================================================ |
@@ -369,6 +451,113 @@ def user_has_project_access( |
369 | 451 | return count > 0 |
370 | 452 |
|
371 | 453 |
|
| 454 | +def invite_client_by_email( |
| 455 | + *, |
| 456 | + session: Session, |
| 457 | + project_id: uuid.UUID, |
| 458 | + email: str, |
| 459 | + role: str = "viewer", |
| 460 | + can_comment: bool = True, |
| 461 | + can_download: bool = True, |
| 462 | +) -> tuple[ProjectAccess | None, bool]: |
| 463 | + """ |
| 464 | + Invite a client to a project by email. |
| 465 | + If user exists: grants immediate access and returns (access, False) |
| 466 | + If user doesn't exist: creates a pending ProjectInvitation and returns (None, True) |
| 467 | + """ |
| 468 | + # Check if user exists |
| 469 | + user = get_user_by_email(session=session, email=email) |
| 470 | + |
| 471 | + if user: |
| 472 | + # User exists - grant immediate access |
| 473 | + # Check if access already exists |
| 474 | + existing_access = get_project_access( |
| 475 | + session=session, project_id=project_id, user_id=user.id |
| 476 | + ) |
| 477 | + if existing_access: |
| 478 | + # Update existing access |
| 479 | + existing_access.role = role |
| 480 | + existing_access.can_comment = can_comment |
| 481 | + existing_access.can_download = can_download |
| 482 | + session.add(existing_access) |
| 483 | + session.commit() |
| 484 | + session.refresh(existing_access) |
| 485 | + return existing_access, False |
| 486 | + |
| 487 | + # Create new access |
| 488 | + access_in = ProjectAccessCreate( |
| 489 | + project_id=project_id, |
| 490 | + user_id=user.id, |
| 491 | + role=role, |
| 492 | + can_comment=can_comment, |
| 493 | + can_download=can_download, |
| 494 | + ) |
| 495 | + access = create_project_access(session=session, access_in=access_in) |
| 496 | + return access, False |
| 497 | + else: |
| 498 | + # User doesn't exist - create pending invitation |
| 499 | + # Check if invitation already exists |
| 500 | + existing_invitation = session.exec( |
| 501 | + select(ProjectInvitation).where( |
| 502 | + ProjectInvitation.project_id == project_id, |
| 503 | + ProjectInvitation.email == email, |
| 504 | + ) |
| 505 | + ).first() |
| 506 | + |
| 507 | + if existing_invitation: |
| 508 | + # Update existing invitation |
| 509 | + existing_invitation.role = role |
| 510 | + existing_invitation.can_comment = can_comment |
| 511 | + existing_invitation.can_download = can_download |
| 512 | + session.add(existing_invitation) |
| 513 | + session.commit() |
| 514 | + session.refresh(existing_invitation) |
| 515 | + return None, True |
| 516 | + |
| 517 | + # Create new invitation |
| 518 | + invitation = ProjectInvitation( |
| 519 | + project_id=project_id, |
| 520 | + email=email, |
| 521 | + role=role, |
| 522 | + can_comment=can_comment, |
| 523 | + can_download=can_download, |
| 524 | + ) |
| 525 | + session.add(invitation) |
| 526 | + session.commit() |
| 527 | + session.refresh(invitation) |
| 528 | + return None, True |
| 529 | + |
| 530 | + |
| 531 | +def process_pending_project_invitations( |
| 532 | + *, session: Session, user_id: uuid.UUID, email: str |
| 533 | +) -> None: |
| 534 | + """ |
| 535 | + Process pending project invitations for a newly created client user. |
| 536 | + Finds all ProjectInvitation records for the email, creates ProjectAccess for each, |
| 537 | + and deletes the invitations. |
| 538 | + """ |
| 539 | + # Find all pending invitations for this email |
| 540 | + statement = select(ProjectInvitation).where(ProjectInvitation.email == email) |
| 541 | + invitations = session.exec(statement).all() |
| 542 | + |
| 543 | + for invitation in invitations: |
| 544 | + # Create project access |
| 545 | + access_in = ProjectAccessCreate( |
| 546 | + project_id=invitation.project_id, |
| 547 | + user_id=user_id, |
| 548 | + role=invitation.role, |
| 549 | + can_comment=invitation.can_comment, |
| 550 | + can_download=invitation.can_download, |
| 551 | + ) |
| 552 | + create_project_access(session=session, access_in=access_in) |
| 553 | + |
| 554 | + # Delete the invitation |
| 555 | + session.delete(invitation) |
| 556 | + |
| 557 | + # Commit all changes |
| 558 | + session.commit() |
| 559 | + |
| 560 | + |
372 | 561 | # ============================================================================ |
373 | 562 | # DASHBOARD STATS |
374 | 563 | # ============================================================================ |
|
0 commit comments