portfolio.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. from fastapi import APIRouter, Depends, HTTPException, Form, UploadFile, File
  2. from typing import Optional
  3. import db
  4. import schemas
  5. import auth_utils
  6. import config
  7. import os
  8. import uuid
  9. import shutil
  10. from services.global_manager import global_manager
  11. from dependencies import require_admin
  12. from services.audit_service import audit_service
  13. router = APIRouter(tags=["portfolio"])
  14. @router.get("/portfolio")
  15. async def get_public_portfolio():
  16. query = """
  17. SELECT p.id, p.file_path, COALESCE(o.material_name, 'Showcase') as material_name, p.order_id, p.caption
  18. FROM order_photos p
  19. LEFT JOIN orders o ON p.order_id = o.id
  20. WHERE p.is_public = TRUE AND (o.id IS NULL OR o.allow_portfolio = TRUE)
  21. ORDER BY p.created_at DESC
  22. """
  23. return db.execute_query(query)
  24. @router.get("/admin/all-photos")
  25. async def admin_get_all_photos(admin: dict = Depends(require_admin)):
  26. query = """
  27. SELECT p.id, p.file_path, p.is_public, p.order_id, p.caption, o.allow_portfolio,
  28. o.first_name, o.last_name, COALESCE(o.material_name, 'Manual') as material_name
  29. FROM order_photos p
  30. LEFT JOIN orders o ON p.order_id = o.id
  31. ORDER BY p.created_at DESC
  32. """
  33. return db.execute_query(query)
  34. @router.post("/admin/orders/{order_id}/photos")
  35. async def admin_upload_order_photo(
  36. order_id: int,
  37. is_public: bool = Form(False),
  38. caption: Optional[str] = Form(None),
  39. file: UploadFile = File(...),
  40. admin: dict = Depends(require_admin)
  41. ):
  42. order = db.execute_query("SELECT allow_portfolio FROM orders WHERE id = %s", (order_id,))
  43. if not order: raise HTTPException(status_code=404, detail="Order not found")
  44. if is_public and not order[0]['allow_portfolio']:
  45. raise HTTPException(status_code=400, detail="Cannot make public: User did not consent to portfolio usage")
  46. if not file.filename: raise HTTPException(status_code=400, detail="Invalid file")
  47. unique_filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
  48. disk_path = os.path.join(config.UPLOAD_DIR, unique_filename)
  49. db_file_path = f"uploads/{unique_filename}"
  50. with open(disk_path, "wb") as buffer:
  51. shutil.copyfileobj(file.file, buffer)
  52. query = "INSERT INTO order_photos (order_id, file_path, is_public, caption) VALUES (%s, %s, %s, %s)"
  53. photo_id = db.execute_commit(query, (order_id, db_file_path, is_public, caption))
  54. await audit_service.log(
  55. user_id=admin['id'],
  56. action="upload_order_photo",
  57. target_type="order",
  58. target_id=order_id,
  59. details={"photo_id": photo_id, "is_public": is_public, "caption": caption}
  60. )
  61. # NOTIFY USER VIA WEBSOCKET
  62. order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
  63. if order_info:
  64. await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
  65. return {"id": photo_id, "file_path": db_file_path, "is_public": is_public, "caption": caption}
  66. @router.post("/admin/portfolio/upload")
  67. async def admin_upload_portfolio_photo(
  68. is_public: bool = Form(True),
  69. caption: Optional[str] = Form(None),
  70. file: UploadFile = File(...),
  71. admin: dict = Depends(require_admin)
  72. ):
  73. if not file.filename: raise HTTPException(status_code=400, detail="Invalid file")
  74. unique_filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
  75. disk_path = os.path.join(config.UPLOAD_DIR, unique_filename)
  76. db_file_path = f"uploads/{unique_filename}"
  77. with open(disk_path, "wb") as buffer:
  78. shutil.copyfileobj(file.file, buffer)
  79. query = "INSERT INTO order_photos (order_id, file_path, is_public, caption) VALUES (%s, %s, %s, %s)"
  80. photo_id = db.execute_commit(query, (None, db_file_path, is_public, caption))
  81. await audit_service.log(
  82. user_id=admin['id'],
  83. action="upload_portfolio_photo",
  84. target_type="photo",
  85. target_id=photo_id,
  86. details={"is_public": is_public, "caption": caption}
  87. )
  88. return {"id": photo_id, "file_path": db_file_path, "is_public": is_public, "caption": caption}
  89. @router.patch("/admin/photos/{photo_id}")
  90. async def admin_update_photo_status(photo_id: int, data: schemas.PhotoUpdate, admin: dict = Depends(require_admin)):
  91. query = "SELECT p.*, o.allow_portfolio FROM order_photos p LEFT JOIN orders o ON p.order_id = o.id WHERE p.id = %s"
  92. photo_data = db.execute_query(query, (photo_id,))
  93. if not photo_data: raise HTTPException(status_code=404, detail="Photo not found")
  94. # Check consent if linked to order
  95. if data.is_public and photo_data[0]['order_id'] is not None and not photo_data[0]['allow_portfolio']:
  96. raise HTTPException(status_code=400, detail="Cannot make public: User did not consent to portfolio usage")
  97. db.execute_commit("UPDATE order_photos SET is_public = %s, caption = %s WHERE id = %s", (data.is_public, data.caption, photo_id))
  98. await audit_service.log(
  99. user_id=admin['id'],
  100. action="update_photo_visibility",
  101. target_type="photo",
  102. target_id=photo_id,
  103. details={"is_public": data.is_public, "caption": data.caption}
  104. )
  105. # NOTIFY USER VIA WEBSOCKET if linked to order
  106. order_id = photo_data[0]['order_id']
  107. if order_id:
  108. order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
  109. if order_info:
  110. await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
  111. return {"id": photo_id, "is_public": data.is_public, "caption": data.caption}
  112. @router.delete("/admin/photos/{photo_id}")
  113. async def admin_delete_photo(photo_id: int, admin: dict = Depends(require_admin)):
  114. photo = db.execute_query("SELECT file_path, order_id FROM order_photos WHERE id = %s", (photo_id,))
  115. if not photo:
  116. raise HTTPException(status_code=404, detail="Photo not found")
  117. order_id = photo[0]['order_id']
  118. try:
  119. path = os.path.join(config.BASE_DIR, photo[0]['file_path'])
  120. if os.path.exists(path):
  121. os.remove(path)
  122. except Exception as e:
  123. print(f"Error deleting photo file: {e}")
  124. db.execute_commit("DELETE FROM order_photos WHERE id = %s", (photo_id,))
  125. await audit_service.log(
  126. user_id=admin['id'],
  127. action="delete_photo",
  128. target_type="photo",
  129. target_id=photo_id,
  130. details={"order_id": order_id}
  131. )
  132. # NOTIFY USER VIA WEBSOCKET
  133. order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
  134. if order_info:
  135. await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
  136. return {"id": photo_id, "status": "deleted"}