perf: paginate docs list, lazy thumbnails, static cache headers
This commit is contained in:
+10
-14
@@ -1,22 +1,20 @@
|
||||
from fastapi import APIRouter, Depends, Request, Form, HTTPException, status
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from services.auth_service import verify_password, get_user_by_username, create_user, delete_user, get_all_users
|
||||
from services.auth_service import (
|
||||
verify_password, get_user_by_username, create_user, delete_user, get_all_users,
|
||||
create_session, get_session, delete_session, SESSION_TTL_SECONDS,
|
||||
)
|
||||
from services.backup_service import create_backup
|
||||
import secrets
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
# Simple memory store for sessions for this requirement. (In prod we use Redis/Cookie etc., but cookie + session dict is quickest without external deps like itsdangerous if not in requirements)
|
||||
sessions = {}
|
||||
|
||||
def get_current_user_from_request(request: Request):
|
||||
session_id = request.cookies.get("session_id")
|
||||
if session_id and session_id in sessions:
|
||||
return sessions[session_id]
|
||||
return None
|
||||
return get_session(session_id) if session_id else None
|
||||
|
||||
def get_current_user(request: Request):
|
||||
user = get_current_user_from_request(request)
|
||||
@@ -37,10 +35,8 @@ async def login_post(request: Request, username: str = Form(...), password: str
|
||||
if not user or not verify_password(user["password_hash"], password):
|
||||
return templates.TemplateResponse(request=request, name="login.html", context={"error": "Invalid username or password"})
|
||||
|
||||
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
sessions[session_id] = dict(user)
|
||||
|
||||
session_id = create_session(user["id"], user["username"])
|
||||
|
||||
from config import ENVIRONMENT
|
||||
# secure=True only when accessed via HTTPS (check X-Forwarded-Proto from nginx)
|
||||
is_https = ENVIRONMENT == "production" and request.headers.get("x-forwarded-proto") == "https"
|
||||
@@ -51,7 +47,7 @@ async def login_post(request: Request, username: str = Form(...), password: str
|
||||
httponly=True,
|
||||
secure=is_https,
|
||||
samesite="lax",
|
||||
max_age=86400, # 24 hours
|
||||
max_age=SESSION_TTL_SECONDS,
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -59,8 +55,8 @@ async def login_post(request: Request, username: str = Form(...), password: str
|
||||
async def logout(request: Request):
|
||||
response = RedirectResponse(url="/auth/login")
|
||||
session_id = request.cookies.get("session_id")
|
||||
if session_id in sessions:
|
||||
del sessions[session_id]
|
||||
if session_id:
|
||||
delete_session(session_id)
|
||||
response.delete_cookie("session_id")
|
||||
return response
|
||||
|
||||
|
||||
+68
-7
@@ -130,6 +130,23 @@ async def delete_document(doc_id: int):
|
||||
if not doc:
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
|
||||
# If other documents were flagged as duplicates of this one, detach them
|
||||
# (promote the earliest orphan to be the new keeper so they remain grouped).
|
||||
orphans = conn.execute(
|
||||
"SELECT id FROM documents WHERE duplicate_of=? ORDER BY id",
|
||||
(doc_id,),
|
||||
).fetchall()
|
||||
if orphans:
|
||||
new_keeper = orphans[0]["id"]
|
||||
conn.execute(
|
||||
"UPDATE documents SET duplicate_of=NULL WHERE id=?",
|
||||
(new_keeper,),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE documents SET duplicate_of=? WHERE duplicate_of=? AND id != ?",
|
||||
(new_keeper, doc_id, new_keeper),
|
||||
)
|
||||
|
||||
# Delete properties for this document
|
||||
conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,))
|
||||
|
||||
@@ -310,22 +327,49 @@ async def scan_duplicates():
|
||||
|
||||
@router.get("/documents/duplicates")
|
||||
async def duplicates_view(request: Request):
|
||||
"""List all documents flagged as duplicates alongside their originals."""
|
||||
"""Group duplicates with their originals so the user can compare side-by-side."""
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT d.*, p.first_name, p.family_name,
|
||||
o.request_number AS orig_request_number,
|
||||
o.created_at AS orig_created_at
|
||||
# Fetch all docs that have been flagged as duplicates + their keepers
|
||||
dup_rows = conn.execute(
|
||||
"""SELECT d.*, p.first_name, p.family_name
|
||||
FROM documents d
|
||||
LEFT JOIN persons p ON p.id = d.person_id
|
||||
LEFT JOIN documents o ON o.id = d.duplicate_of
|
||||
WHERE d.duplicate_of IS NOT NULL
|
||||
ORDER BY d.duplicate_of, d.id"""
|
||||
).fetchall()
|
||||
|
||||
keeper_ids = sorted({r["duplicate_of"] for r in dup_rows if r["duplicate_of"]})
|
||||
keepers_by_id = {}
|
||||
if keeper_ids:
|
||||
placeholders = ",".join("?" * len(keeper_ids))
|
||||
keeper_rows = conn.execute(
|
||||
f"""SELECT d.*, p.first_name, p.family_name
|
||||
FROM documents d
|
||||
LEFT JOIN persons p ON p.id = d.person_id
|
||||
WHERE d.id IN ({placeholders})""",
|
||||
keeper_ids,
|
||||
).fetchall()
|
||||
keepers_by_id = {r["id"]: dict(r) for r in keeper_rows}
|
||||
|
||||
groups = []
|
||||
seen_keepers = []
|
||||
dups_by_keeper: dict[int, list[dict]] = {}
|
||||
for r in dup_rows:
|
||||
k = r["duplicate_of"]
|
||||
dups_by_keeper.setdefault(k, []).append(dict(r))
|
||||
if k not in seen_keepers:
|
||||
seen_keepers.append(k)
|
||||
|
||||
for k in seen_keepers:
|
||||
keeper = keepers_by_id.get(k)
|
||||
if not keeper:
|
||||
continue
|
||||
groups.append({"keeper": keeper, "duplicates": dups_by_keeper.get(k, [])})
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"duplicates.html",
|
||||
{"documents": [dict(r) for r in rows]},
|
||||
{"groups": groups, "total_duplicates": len(dup_rows)},
|
||||
)
|
||||
|
||||
|
||||
@@ -347,3 +391,20 @@ async def delete_all_duplicates():
|
||||
pass
|
||||
deleted += 1
|
||||
return JSONResponse({"ok": True, "deleted": deleted})
|
||||
|
||||
|
||||
@router.post("/documents/{doc_id}/unflag-duplicate")
|
||||
async def unflag_duplicate(doc_id: int):
|
||||
"""Mark a flagged-duplicate document as NOT a duplicate (clear duplicate_of)."""
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id FROM documents WHERE id=? AND duplicate_of IS NOT NULL",
|
||||
(doc_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return JSONResponse({"error": "not flagged"}, status_code=404)
|
||||
conn.execute(
|
||||
"UPDATE documents SET duplicate_of=NULL, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
(doc_id,),
|
||||
)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
Reference in New Issue
Block a user