Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
.idea
venv
.venv
*.db
*.db
src/inputs/*.pdf
8 changes: 7 additions & 1 deletion api/db/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ def create_form(session: Session, form: FormSubmission) -> FormSubmission:
session.add(form)
session.commit()
session.refresh(form)
return form
return form

def get_all_templates(session: Session, limit: int = 100, offset: int = 0) -> list[Template]:
return session.exec(select(Template).offset(offset).limit(limit)).all()

def get_form(session: Session, submission_id: int) -> FormSubmission | None:
return session.get(FormSubmission, submission_id)
20 changes: 19 additions & 1 deletion api/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from api.routes import templates, forms
from api.errors.base import AppError
from typing import Union

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)

@app.exception_handler(AppError)
def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.message}
)

app.include_router(templates.router)
app.include_router(forms.router)
69 changes: 63 additions & 6 deletions api/routes/forms.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,82 @@
import os
from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
from sqlmodel import Session
from api.deps import get_db
from api.schemas.forms import FormFill, FormFillResponse
from api.db.repositories import create_form, get_template
from api.db.repositories import create_form, get_template, get_form
from api.db.models import FormSubmission
from api.errors.base import AppError
from src.controller import Controller

router = APIRouter(prefix="/forms", tags=["forms"])


@router.post("/fill", response_model=FormFillResponse)
def fill_form(form: FormFill, db: Session = Depends(get_db)):
if not get_template(db, form.template_id):
# Single DB query (fixes issue #149 - redundant query)
template = get_template(db, form.template_id)
if not template:
raise AppError("Template not found", status_code=404)

fetched_template = get_template(db, form.template_id)
try:
controller = Controller()
# FileManipulator.fill_form expects fields as a list of key strings
fields_list = list(template.fields.keys()) if isinstance(template.fields, dict) else template.fields
path = controller.fill_form(
user_input=form.input_text,
fields=fields_list,
pdf_form_path=template.pdf_path
)
except ConnectionError:
raise AppError(
"Could not connect to Ollama. Make sure ollama serve is running.",
status_code=503
)
except Exception as e:
raise AppError(f"PDF filling failed: {str(e)}", status_code=500)

# Guard: controller returned None instead of a file path
if not path:
raise AppError(
"PDF generation failed — no output file was produced. "
"Check that the PDF template is a valid fillable form and Ollama is running.",
status_code=500
)

controller = Controller()
path = controller.fill_form(user_input=form.input_text, fields=fetched_template.fields, pdf_form_path=fetched_template.pdf_path)
if not os.path.exists(path):
raise AppError(
f"PDF was generated but file not found at: {path}",
status_code=500
)

submission = FormSubmission(**form.model_dump(), output_pdf_path=path)
submission = FormSubmission(
**form.model_dump(),
output_pdf_path=path
)
return create_form(db, submission)


@router.get("/{submission_id}", response_model=FormFillResponse)
def get_submission(submission_id: int, db: Session = Depends(get_db)):
submission = get_form(db, submission_id)
if not submission:
raise AppError("Submission not found", status_code=404)
return submission


@router.get("/download/{submission_id}")
def download_filled_pdf(submission_id: int, db: Session = Depends(get_db)):
submission = get_form(db, submission_id)
if not submission:
raise AppError("Submission not found", status_code=404)

file_path = submission.output_pdf_path
if not os.path.exists(file_path):
raise AppError("PDF file not found on server", status_code=404)

return FileResponse(
path=file_path,
media_type="application/pdf",
filename=os.path.basename(file_path)
)
91 changes: 82 additions & 9 deletions api/routes/templates.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,89 @@
from fastapi import APIRouter, Depends
import os
import shutil
import uuid
from fastapi import APIRouter, Depends, UploadFile, File, Form
from sqlmodel import Session
from api.deps import get_db
from api.schemas.templates import TemplateCreate, TemplateResponse
from api.db.repositories import create_template
from api.schemas.templates import TemplateResponse
from api.db.repositories import create_template, get_all_templates
from api.db.models import Template
from src.controller import Controller
from api.errors.base import AppError

router = APIRouter(prefix="/templates", tags=["templates"])

# Save directly into src/inputs/ — stable location, won't get wiped
TEMPLATES_DIR = os.path.join("src", "inputs")
os.makedirs(TEMPLATES_DIR, exist_ok=True)


@router.post("/create", response_model=TemplateResponse)
def create(template: TemplateCreate, db: Session = Depends(get_db)):
controller = Controller()
template_path = controller.create_template(template.pdf_path)
tpl = Template(**template.model_dump(exclude={"pdf_path"}), pdf_path=template_path)
return create_template(db, tpl)
async def create(
name: str = Form(...),
file: UploadFile = File(...),
db: Session = Depends(get_db)
):
# Validate PDF
if not file.filename.endswith(".pdf"):
raise AppError("Only PDF files are allowed", status_code=400)

# Save uploaded file with unique name into src/inputs/
unique_name = f"{uuid.uuid4().hex}_{file.filename}"
save_path = os.path.join(TEMPLATES_DIR, unique_name)

with open(save_path, "wb") as f:
shutil.copyfileobj(file.file, f)

# Extract fields using commonforms + pypdf
# Store as simple list of field name strings — what Filler expects
try:
from commonforms import prepare_form
from pypdf import PdfReader

# Read real field names directly from original PDF
# Use /T (internal name) as both key and label
# Real names like "JobTitle", "Phone Number" are already human-readable
reader = PdfReader(save_path)
raw_fields = reader.get_fields() or {}

fields = {}
for internal_name, field_data in raw_fields.items():
# Use /TU tooltip if available, otherwise prettify /T name
label = None
if isinstance(field_data, dict):
label = field_data.get("/TU")
if not label:
# Prettify: "JobTitle" → "Job Title", "DATE7_af_date" → "Date"
import re
label = re.sub(r'([a-z])([A-Z])', r'\1 \2', internal_name)
label = re.sub(r'_af_.*$', '', label) # strip "_af_date" suffix
label = label.replace('_', ' ').strip().title()
fields[internal_name] = label

except Exception as e:
print(f"Field extraction failed: {e}")
fields = []

# Save to DB
tpl = Template(name=name, pdf_path=save_path, fields=fields)
return create_template(db, tpl)


@router.get("", response_model=list[TemplateResponse])
def list_templates(
limit: int = 100,
offset: int = 0,
db: Session = Depends(get_db)
):
return get_all_templates(db, limit=limit, offset=offset)


@router.get("/{template_id}", response_model=TemplateResponse)
def get_template_by_id(
template_id: int,
db: Session = Depends(get_db)
):
from api.db.repositories import get_template
tpl = get_template(db, template_id)
if not tpl:
raise AppError("Template not found", status_code=404)
return tpl
Loading