feat: per-position camera settings, material alias dialog, product delete, media browser links
- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -270,12 +270,73 @@ async def delete_product(
|
||||
raise HTTPException(404, detail="Product not found")
|
||||
if hard:
|
||||
from sqlalchemy import delete as sql_delete
|
||||
# Delete order_lines referencing this product
|
||||
from app.domains.media.models import MediaAsset
|
||||
from app.core.storage import get_storage
|
||||
|
||||
# 1. Collect storage keys from MediaAssets before cascade deletes them
|
||||
media_result = await db.execute(
|
||||
select(MediaAsset.storage_key).where(MediaAsset.product_id == product_id)
|
||||
)
|
||||
storage_keys = [row[0] for row in media_result.all() if row[0]]
|
||||
|
||||
# 2. Collect render result paths from order lines
|
||||
ol_result = await db.execute(
|
||||
select(OrderLine.result_path).where(
|
||||
OrderLine.product_id == product_id,
|
||||
OrderLine.result_path.isnot(None),
|
||||
)
|
||||
)
|
||||
result_paths = [row[0] for row in ol_result.all() if row[0]]
|
||||
|
||||
# 3. Check if CadFile is used by other products
|
||||
cad_file_id = product.cad_file_id
|
||||
orphan_cad = False
|
||||
if cad_file_id:
|
||||
other_count = await db.execute(
|
||||
select(func.count(Product.id)).where(
|
||||
Product.cad_file_id == cad_file_id,
|
||||
Product.id != product_id,
|
||||
)
|
||||
)
|
||||
orphan_cad = (other_count.scalar() or 0) == 0
|
||||
|
||||
# 4. Delete order_lines referencing this product
|
||||
await db.execute(sql_delete(OrderLine).where(OrderLine.product_id == product_id))
|
||||
|
||||
# 5. Delete orphaned CadFile if no other products reference it
|
||||
if orphan_cad and cad_file_id:
|
||||
from app.models.cad_file import CadFile
|
||||
# Collect CadFile media assets too
|
||||
cad_media_result = await db.execute(
|
||||
select(MediaAsset.storage_key).where(MediaAsset.cad_file_id == cad_file_id)
|
||||
)
|
||||
storage_keys.extend(row[0] for row in cad_media_result.all() if row[0])
|
||||
product.cad_file_id = None
|
||||
await db.flush()
|
||||
await db.execute(sql_delete(CadFile).where(CadFile.id == cad_file_id))
|
||||
|
||||
# 6. Delete product (cascades MediaAsset + ProductRenderPosition)
|
||||
await db.delete(product)
|
||||
await db.commit()
|
||||
|
||||
# 7. Clean up storage files (best-effort, after commit)
|
||||
storage = get_storage()
|
||||
for key in storage_keys:
|
||||
try:
|
||||
storage.delete(key)
|
||||
except Exception:
|
||||
pass
|
||||
# Clean up render result files on disk
|
||||
import os
|
||||
for path in result_paths:
|
||||
try:
|
||||
if os.path.isfile(path):
|
||||
os.unlink(path)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
product.is_active = False
|
||||
await db.commit()
|
||||
await db.commit()
|
||||
|
||||
|
||||
@router.post("/{product_id}/cad", status_code=status.HTTP_201_CREATED)
|
||||
|
||||
Reference in New Issue
Block a user