feat: Kanban shows each order's artwork, filters quotes and searches orders and quotes
All checks were successful
Build and deploy / Validate source (push) Successful in 1m25s
Build and deploy / Integration suite on a real stack (push) Successful in 3m16s
Build and deploy / Secret scan and release gate (push) Successful in 13s
Build and deploy / Publish images (push) Successful in 1m29s
All checks were successful
Build and deploy / Validate source (push) Successful in 1m25s
Build and deploy / Integration suite on a real stack (push) Successful in 3m16s
Build and deploy / Secret scan and release gate (push) Successful in 13s
Build and deploy / Publish images (push) Successful in 1m29s
The customer's browser sends the small picture it already makes of each file (at most 300 KB, WebP/JPEG/PNG read from the bytes, own uploads only); it goes when the file's bytes go. Board cards, quote rows, the order panel and the quote detail show it, with the layout drawing as a second view and as the fallback for files without a picture. Cotações gets a customer filter (e-mail, CNPJ, WhatsApp) and product, layout and resolution-warning chips, with the page bar always shown. The board adds Entrega, Retirada and stalled-order chips. The top search now covers orders in any stage and unpaid quotes; CNPJ and phone match by digits only when nothing but digits and punctuation was typed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@ import os
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from ..core import db
|
||||
from ..core.auth import audit, owner, rate_limit
|
||||
@@ -94,6 +95,40 @@ def complete_upload(uid: UUID, session_id=Depends(owner)):
|
||||
(UNPAID_HOLD, uid))
|
||||
return {'id': uid, 'complete': True}
|
||||
|
||||
# The browser's small picture of the artwork, for the Kanban. Only an image, and
|
||||
# only a small one: the type is read from the bytes, never from the header.
|
||||
THUMBNAIL_BYTES = 300_000
|
||||
|
||||
def thumbnail_type(data):
|
||||
if data[:4] == b'RIFF' and data[8:12] == b'WEBP':
|
||||
return 'image/webp'
|
||||
if data[:3] == b'\xff\xd8\xff':
|
||||
return 'image/jpeg'
|
||||
if data[:8] == b'\x89PNG\r\n\x1a\n':
|
||||
return 'image/png'
|
||||
return None
|
||||
|
||||
def store_thumbnail(uid, session_id, mime, data):
|
||||
rate_limit('upload-thumbnail', str(session_id), 120, 900)
|
||||
with db.connect() as c:
|
||||
upload_row(c, uid, session_id)
|
||||
c.execute('''INSERT INTO dtf_local.upload_thumbnails(upload_id,mime,data) VALUES(%s,%s,%s)
|
||||
ON CONFLICT(upload_id) DO UPDATE SET mime=EXCLUDED.mime, data=EXCLUDED.data, created_at=now()''',
|
||||
(uid, mime, data))
|
||||
|
||||
@router.put('/api/uploads/{uid}/thumbnail')
|
||||
async def put_thumbnail(uid: UUID, request: Request, session_id=Depends(owner)):
|
||||
if int(request.headers.get('content-length') or 0) > THUMBNAIL_BYTES:
|
||||
raise HTTPException(413, 'Thumbnail too large')
|
||||
data = await request.body()
|
||||
if len(data) > THUMBNAIL_BYTES:
|
||||
raise HTTPException(413, 'Thumbnail too large')
|
||||
mime = thumbnail_type(data)
|
||||
if not mime:
|
||||
raise HTTPException(415, 'Thumbnail must be a WebP, JPEG or PNG image')
|
||||
await run_in_threadpool(store_thumbnail, uid, session_id, mime, data)
|
||||
return {'id': uid}
|
||||
|
||||
@router.delete('/api/uploads/{uid}')
|
||||
def cancel_upload(uid: UUID, session_id=Depends(owner)):
|
||||
with db.connect() as c:
|
||||
@@ -102,5 +137,6 @@ def cancel_upload(uid: UUID, session_id=Depends(owner)):
|
||||
raise HTTPException(409, 'Completed upload cannot be cancelled')
|
||||
storage.discard(row['object_key'],row['multipart_id'],False)
|
||||
c.execute('UPDATE dtf_local.uploads SET purged_at=now() WHERE id=%s',(uid,))
|
||||
c.execute('DELETE FROM dtf_local.upload_thumbnails WHERE upload_id=%s',(uid,))
|
||||
audit('upload_cancelled', upload=str(uid))
|
||||
return {'id':uid,'cancelled':True}
|
||||
|
||||
Reference in New Issue
Block a user