diff --git a/routers/auth.py b/routers/auth.py index 78af952..7fc187b 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends, Request, Form, HTTPException, status from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse from fastapi.templating import Jinja2Templates +from services.template_utils import render_template 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, @@ -45,7 +46,7 @@ def require_admin(request: Request): @router.get("/login", response_class=HTMLResponse) async def login_get(request: Request): - return templates.TemplateResponse("login.html", {"request": request, "error": None}) + return render_template(templates, request, "login.html", {"error": None}) @router.post("/login", response_class=HTMLResponse) async def login_post(request: Request, username: str = Form(...), password: str = Form(...)): @@ -53,18 +54,22 @@ async def login_post(request: Request, username: str = Form(...), password: str ip = _client_ip(request) if is_login_blocked(username, ip): - return templates.TemplateResponse( + return render_template( + templates, + request, "login.html", - {"request": request, "error": "تم حجب محاولات تسجيل الدخول مؤقتاً. حاول بعد 15 دقيقة."}, + {"error": "تم حجب محاولات تسجيل الدخول مؤقتاً. حاول بعد 15 دقيقة."}, status_code=429, ) user = get_user_by_username(username) if not user or not verify_password(user["password_hash"], password): record_login_attempt(username, ip, success=False) - return templates.TemplateResponse( + return render_template( + templates, + request, "login.html", - {"request": request, "error": "Invalid username or password"}, + {"error": "Invalid username or password"}, status_code=401, ) @@ -106,10 +111,7 @@ async def logout(request: Request): @router.get("/users", response_class=HTMLResponse) async def users_list(request: Request, current=Depends(require_admin)): users = get_all_users() - return templates.TemplateResponse( - "users.html", - {"request": request, "users": users, "current_user": current}, - ) + return render_template(templates, request, "users.html", {"users": users, "current_user": current}) @router.post("/users/create") async def add_user( @@ -124,15 +126,19 @@ async def add_user( role = "user" if len(password) < 8: users = get_all_users() - return templates.TemplateResponse( + return render_template( + templates, + request, "users.html", - {"request": request, "users": users, "current_user": current, "error": "Password must be at least 8 characters."}, + {"users": users, "current_user": current, "error": "Password must be at least 8 characters."}, ) if get_user_by_username(username): users = get_all_users() - return templates.TemplateResponse( + return render_template( + templates, + request, "users.html", - {"request": request, "users": users, "current_user": current, "error": f"User '{username}' already exists."}, + {"users": users, "current_user": current, "error": f"User '{username}' already exists."}, ) create_user(username, password, role=role) return RedirectResponse(url="/auth/users", status_code=status.HTTP_303_SEE_OTHER) @@ -141,18 +147,22 @@ async def add_user( async def remove_user(request: Request, user_id: int, current=Depends(require_admin)): if user_id == current["user_id"]: users = get_all_users() - return templates.TemplateResponse( + return render_template( + templates, + request, "users.html", - {"request": request, "users": users, "current_user": current, "error": "لا يمكنك حذف حسابك الخاص."}, + {"users": users, "current_user": current, "error": "لا يمكنك حذف حسابك الخاص."}, status_code=400, ) # Prevent removing the last admin target = next((u for u in get_all_users() if u["id"] == user_id), None) if target and target["role"] == "admin" and count_admins() <= 1: users = get_all_users() - return templates.TemplateResponse( + return render_template( + templates, + request, "users.html", - {"request": request, "users": users, "current_user": current, "error": "لا يمكن حذف آخر مسؤول في النظام."}, + {"users": users, "current_user": current, "error": "لا يمكن حذف آخر مسؤول في النظام."}, status_code=400, ) delete_user(user_id) @@ -172,9 +182,11 @@ async def change_role( target = next((u for u in get_all_users() if u["id"] == user_id), None) if target and target["role"] == "admin" and role != "admin" and count_admins() <= 1: users = get_all_users() - return templates.TemplateResponse( + return render_template( + templates, + request, "users.html", - {"request": request, "users": users, "current_user": current, "error": "لا يمكن تخفيض رتبة آخر مسؤول."}, + {"users": users, "current_user": current, "error": "لا يمكن تخفيض رتبة آخر مسؤول."}, status_code=400, ) set_user_role(user_id, role) @@ -187,10 +199,11 @@ async def backup_db(request: Request, current=Depends(require_admin)): return FileResponse(backup_path, media_type="application/octet-stream", filename=os.path.basename(backup_path)) except Exception as e: msg = f"Backup failed: {str(e)}" - return templates.TemplateResponse( + return render_template( + templates, + request, "users.html", { - "request": request, "users": get_all_users(), "current_user": current, "backup_msg": msg, diff --git a/routers/documents.py b/routers/documents.py index 4d7ee05..87cf243 100644 --- a/routers/documents.py +++ b/routers/documents.py @@ -7,6 +7,7 @@ from fastapi.templating import Jinja2Templates from config import UPLOAD_DIR from database.connection import get_db +from services.template_utils import render_template from services.thumbnails import get_or_create_thumbnail router = APIRouter() @@ -103,10 +104,11 @@ async def document_queue( total_pages = max(1, (total_filtered + PAGE_SIZE - 1) // PAGE_SIZE) - return templates.TemplateResponse( + return render_template( + templates, + request, "documents.html", { - "request": request, "documents": [dict(r) for r in rows], "stats": dict(stats) if stats else {}, "current_status": status, @@ -368,9 +370,11 @@ async def duplicates_view(request: Request): continue groups.append({"keeper": keeper, "duplicates": dups_by_keeper.get(k, [])}) - return templates.TemplateResponse( + return render_template( + templates, + request, "duplicates.html", - {"request": request, "groups": groups, "total_duplicates": len(dup_rows)}, + {"groups": groups, "total_duplicates": len(dup_rows)}, ) diff --git a/routers/review.py b/routers/review.py index 46ab914..c9eb56f 100644 --- a/routers/review.py +++ b/routers/review.py @@ -16,6 +16,7 @@ from services.extractor import ( verify_page_correlation, ) from services.search_service import normalize_arabic, _normalize_scope +from services.template_utils import render_template router = APIRouter() templates = Jinja2Templates(directory="templates") @@ -328,10 +329,11 @@ async def review_document(request: Request, doc_id: int, wait: int = 0): """ ) - return templates.TemplateResponse( + return render_template( + templates, + request, "review.html", { - "request": request, "doc": doc, "upload_dir": "/uploads", "providers": get_available_providers(), diff --git a/routers/search.py b/routers/search.py index 45b7846..e0c3618 100644 --- a/routers/search.py +++ b/routers/search.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Request from fastapi.responses import Response from fastapi.templating import Jinja2Templates +from services.template_utils import render_template from services.search_service import ( get_person_with_properties, @@ -37,10 +38,11 @@ async def search( persons[0].get("search_scope"), ) - return templates.TemplateResponse( + return render_template( + templates, + request, "search.html", { - "request": request, "q": q, "property_number": property_number, "district": district, @@ -56,10 +58,11 @@ async def search( async def person_detail(request: Request, person_id: int, search_scope: str = ""): data = get_person_with_properties(person_id, search_scope.strip() or None) if not data: - return templates.TemplateResponse( + return render_template( + templates, + request, "search.html", { - "request": request, "error": "Person not found", "q": "", "persons": [], @@ -71,10 +74,7 @@ async def person_detail(request: Request, person_id: int, search_scope: str = "" }, status_code=404, ) - return templates.TemplateResponse( - "person_detail.html", - {"request": request, **data}, - ) + return render_template(templates, request, "person_detail.html", data) @router.get("/persons/{person_id}/export") diff --git a/routers/upload.py b/routers/upload.py index db26a6a..144868b 100644 --- a/routers/upload.py +++ b/routers/upload.py @@ -13,6 +13,7 @@ from config import UPLOAD_DIR, MAX_CONCURRENT_EXTRACTIONS from database.connection import get_db from services.extractor import extract_document, get_available_providers, get_default_provider from services.pdf_handler import pdf_to_images +from services.template_utils import render_template router = APIRouter() templates = Jinja2Templates(directory="templates") @@ -204,10 +205,11 @@ async def upload_page(request: Request): SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) AS errors FROM documents WHERE status != 'staged'""" ).fetchone() - return templates.TemplateResponse( + return render_template( + templates, + request, "index.html", { - "request": request, "stats": dict(stats) if stats else {}, "providers": get_available_providers(), "default_provider": get_default_provider(), diff --git a/services/template_utils.py b/services/template_utils.py new file mode 100644 index 0000000..4251935 --- /dev/null +++ b/services/template_utils.py @@ -0,0 +1,13 @@ +import inspect +from collections.abc import Mapping + + +def render_template(templates, request, template_name: str, context: Mapping | None = None, **response_kwargs): + context_dict = dict(context or {}) + parameters = list(inspect.signature(templates.TemplateResponse).parameters) + + if parameters and parameters[0] == "request": + return templates.TemplateResponse(request, template_name, context_dict, **response_kwargs) + + context_dict.setdefault("request", request) + return templates.TemplateResponse(template_name, context_dict, **response_kwargs) \ No newline at end of file