diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index 8603be1..5896373 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ diff --git a/main.py b/main.py index eac1f5d..5b5df4b 100644 --- a/main.py +++ b/main.py @@ -544,7 +544,11 @@ async def generate_cvs_for_requirement(req_id: str, request: Request): @app.get("/api/generated-cvs") async def list_generated_cvs(req_id: str = None, candidate_id: str = None, batch_id: str = None): """List batch-generated CV exports (plus any legacy generated_cvs rows).""" - where_parts = ["bi.generated_cv_path IS NOT NULL", "bi.generated_cv_path <> ''"] + where_parts = [ + "bi.generated_cv_path IS NOT NULL", + "bi.generated_cv_path <> ''", + "bi.status = 'approved'", + ] params = [] if candidate_id: where_parts.append("bi.candidate_id = %s") @@ -901,7 +905,11 @@ async def list_batches(): COUNT(*) FILTER (WHERE status = 'approved') AS approved_items, COUNT(*) FILTER (WHERE status = 'proposed') AS proposed_items, COUNT(*) FILTER (WHERE status = 'removed') AS removed_items, - COUNT(*) FILTER (WHERE generated_cv_path IS NOT NULL AND generated_cv_path <> '') AS generated_items + COUNT(*) FILTER ( + WHERE generated_cv_path IS NOT NULL + AND generated_cv_path <> '' + AND status = 'approved' + ) AS generated_items FROM batch_items GROUP BY batch_id ) stats ON stats.batch_id = b.id @@ -1070,11 +1078,41 @@ async def analyze_batch(batch_id: str): async def update_batch_item(batch_id: str, item_id: str, request: Request): """Update a batch item (approve, remove, edit realignment).""" body = await request.json() - row = db.execute( - """UPDATE batch_items SET status = %s, realigned_cv_data = %s, updated_at = NOW() - WHERE id = %s AND batch_id = %s RETURNING *""", - (body.get("status", "proposed"), Json(body.get("realigned_cv_data")), item_id, batch_id) - ) + status = body.get("status", "proposed") + realigned = body.get("realigned_cv_data") + + # If removing a candidate, clear any stale generated PDF path so exports/ZIPs stay clean. + if status == "removed": + existing = db.query( + "SELECT generated_cv_path FROM batch_items WHERE id = %s AND batch_id = %s", + (item_id, batch_id), + fetch="one", + ) + if existing and existing.get("generated_cv_path"): + rel = str(existing["generated_cv_path"]).lstrip("/") + if rel.startswith("static/"): + file_path = static_dir / rel[len("static/"):] + else: + file_path = static_dir / "generated" / Path(rel).name + try: + if file_path.exists() and file_path.is_file(): + # Only delete files that belong to this item id tag when possible. + if str(item_id)[:8] in file_path.name: + file_path.unlink() + except Exception: + pass + row = db.execute( + """UPDATE batch_items + SET status = %s, realigned_cv_data = %s, generated_cv_path = NULL, updated_at = NOW() + WHERE id = %s AND batch_id = %s RETURNING *""", + (status, Json(realigned), item_id, batch_id), + ) + else: + row = db.execute( + """UPDATE batch_items SET status = %s, realigned_cv_data = %s, updated_at = NOW() + WHERE id = %s AND batch_id = %s RETURNING *""", + (status, Json(realigned), item_id, batch_id), + ) if not row: raise HTTPException(404, "Batch item not found") return dict(row) @@ -1183,7 +1221,26 @@ async def update_batch_positions(batch_id: str, request: Request): async def generate_batch_cvs(batch_id: str): """Generate CVs for all approved items in the batch using the selected template.""" import httpx - + from datetime import date as _date, datetime as _datetime + from decimal import Decimal + from uuid import UUID + + def _jsonable(value): + """Recursively convert DB row values into JSON-safe types.""" + if isinstance(value, dict): + return {k: _jsonable(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [_jsonable(v) for v in value] + if isinstance(value, (_datetime, _date)): + return value.isoformat() + if isinstance(value, UUID): + return str(value) + if isinstance(value, Decimal): + return float(value) + if isinstance(value, (bytes, bytearray)): + return value.decode("utf-8", errors="replace") + return value + batch = db.query("SELECT * FROM cv_batches WHERE id = %s", (batch_id,), fetch='one') if not batch: raise HTTPException(404, "Batch not found") @@ -1218,6 +1275,7 @@ async def generate_batch_cvs(batch_id: str): cand["experience"] = [dict(e) for e in db.query("SELECT * FROM experience WHERE candidate_id = %s", (candidate_id,))] cand["education"] = [dict(e) for e in db.query("SELECT * FROM education WHERE candidate_id = %s", (candidate_id,))] cand["certifications"] = [dict(c) for c in db.query("SELECT * FROM certifications WHERE candidate_id = %s", (candidate_id,))] + cand = _jsonable(cand) # Use realigned data if available, else original realigned = item.get("realigned_cv_data") @@ -1231,11 +1289,13 @@ async def generate_batch_cvs(batch_id: str): if resp.status_code == 200: # Save the PDF - import tempfile pdf_dir = os.path.join(str(static_dir), "generated") os.makedirs(pdf_dir, exist_ok=True) safe_name = f"{item['first_name']}_{item['last_name']}".replace(" ", "_") - filename = f"{batch['name'].replace(' ', '_')}_{safe_name}_{candidate_id[:8]}.pdf" + safe_batch = str(batch['name']).replace(" ", "_").replace("/", "-") + safe_pos = str(item.get("position_title") or "role").replace(" ", "_").replace("/", "-")[:40] + item_tag = str(item["id"])[:8] + filename = f"{safe_batch}_{safe_pos}_{safe_name}_{item_tag}.pdf" filepath = os.path.join(pdf_dir, filename) with open(filepath, "wb") as f: f.write(resp.content) @@ -1245,16 +1305,24 @@ async def generate_batch_cvs(batch_id: str): "UPDATE batch_items SET generated_cv_path = %s, updated_at = NOW() WHERE id = %s", (f"/static/generated/{filename}", item["id"]) ) - results.append({"candidate": f"{item['first_name']} {item['last_name']}", "status": "ok", "file": filename}) + results.append({ + "candidate": f"{item['first_name']} {item['last_name']}", + "position_title": item.get("position_title"), + "status": "ok", + "file": filename, + "path": f"/static/generated/{filename}", + }) else: results.append({"candidate": f"{item['first_name']} {item['last_name']}", "status": "error", "error": resp.text[:200]}) except Exception as e: results.append({"candidate": f"{item['first_name']} {item['last_name']}", "status": "error", "error": str(e)}) - # Update batch status to exported - db.execute("UPDATE cv_batches SET status = 'exported', updated_at = NOW() WHERE id = %s", (batch_id,)) + ok_count = len([r for r in results if r["status"] == "ok"]) + # Only mark exported if at least one CV succeeded + if ok_count: + db.execute("UPDATE cv_batches SET status = 'exported', updated_at = NOW() WHERE id = %s", (batch_id,)) - return {"success": True, "generated": len([r for r in results if r["status"] == "ok"]), "results": results} + return {"success": True, "generated": ok_count, "results": results} @app.get("/api/batches/{batch_id}/download-zip") @@ -1273,6 +1341,7 @@ async def download_batch_zip(batch_id: str): FROM batch_items bi JOIN candidates c ON c.id = bi.candidate_id WHERE bi.batch_id = %s + AND bi.status = 'approved' AND bi.generated_cv_path IS NOT NULL AND bi.generated_cv_path <> '' ORDER BY bi.position_title, c.last_name, c.first_name diff --git a/static/generated/Portal_Modernisation_Support_Education_-_Training_Specialist_Jamie_Smith_d3527e16.pdf b/static/generated/Portal_Modernisation_Support_Education_-_Training_Specialist_Jamie_Smith_d3527e16.pdf new file mode 100644 index 0000000..a4b8841 Binary files /dev/null and b/static/generated/Portal_Modernisation_Support_Education_-_Training_Specialist_Jamie_Smith_d3527e16.pdf differ diff --git a/static/generated/Portal_Modernisation_Support_Senior_Software_Engineer_John_Smith_672fd357.pdf b/static/generated/Portal_Modernisation_Support_Senior_Software_Engineer_John_Smith_672fd357.pdf new file mode 100644 index 0000000..83c5c5f Binary files /dev/null and b/static/generated/Portal_Modernisation_Support_Senior_Software_Engineer_John_Smith_672fd357.pdf differ diff --git a/static/generated/Portal_Modernisation_Support_Technical_Lead_-_Architect_Jaco_Smith_2ea89769.pdf b/static/generated/Portal_Modernisation_Support_Technical_Lead_-_Architect_Jaco_Smith_2ea89769.pdf new file mode 100644 index 0000000..1b84665 Binary files /dev/null and b/static/generated/Portal_Modernisation_Support_Technical_Lead_-_Architect_Jaco_Smith_2ea89769.pdf differ