Fix 503: make xhtml2pdf import lazy to prevent app crash

If xhtml2pdf fails to install on Flatlogic's server (missing C
libraries), the top-level import crashed the entire WSGI app.
Now it imports lazily inside render_to_pdf() so the app starts
even without xhtml2pdf — only PDF generation degrades gracefully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-02-22 21:07:33 +02:00
parent 71723dcaf4
commit 74cd93fede
2 changed files with 27 additions and 3 deletions

View File

@ -1,10 +1,17 @@
# === PDF GENERATION === # === PDF GENERATION ===
# Converts a Django HTML template into a PDF file using xhtml2pdf. # Converts a Django HTML template into a PDF file using xhtml2pdf.
# Used for payslip and receipt PDF attachments sent via email. # Used for payslip and receipt PDF attachments sent via email.
#
# IMPORTANT: xhtml2pdf is imported LAZILY (inside the function, not at the
# top of the file). This is intentional — if xhtml2pdf fails to install on
# the server (missing C libraries), the rest of the app still works.
# Only PDF generation will fail gracefully.
import logging
from io import BytesIO from io import BytesIO
from django.template.loader import get_template from django.template.loader import get_template
from xhtml2pdf import pisa
logger = logging.getLogger(__name__)
def render_to_pdf(template_src, context_dict=None): def render_to_pdf(template_src, context_dict=None):
@ -21,6 +28,17 @@ def render_to_pdf(template_src, context_dict=None):
if context_dict is None: if context_dict is None:
context_dict = {} context_dict = {}
# --- Lazy import: only load xhtml2pdf when actually generating a PDF ---
# This prevents the entire app from crashing if xhtml2pdf isn't installed.
try:
from xhtml2pdf import pisa
except ImportError:
logger.error(
"xhtml2pdf is not installed — cannot generate PDF. "
"Install it with: pip install xhtml2pdf"
)
return None
# Load and render the HTML template # Load and render the HTML template
template = get_template(template_src) template = get_template(template_src)
html = template.render(context_dict) html = template.render(context_dict)

View File

@ -22,7 +22,9 @@ from django.conf import settings
from .models import Worker, Project, WorkLog, Team, PayrollRecord, Loan, PayrollAdjustment from .models import Worker, Project, WorkLog, Team, PayrollRecord, Loan, PayrollAdjustment
from .forms import AttendanceLogForm, PayrollAdjustmentForm from .forms import AttendanceLogForm, PayrollAdjustmentForm
from .utils import render_to_pdf # NOTE: render_to_pdf is NOT imported here at the top level.
# It's imported lazily inside process_payment() to avoid crashing the
# entire app if xhtml2pdf is not installed on the server.
# === PAYROLL CONSTANTS === # === PAYROLL CONSTANTS ===
@ -812,6 +814,10 @@ def process_payment(request, worker_id):
# EMAIL PAYSLIP (outside the transaction — if email fails, payment is # EMAIL PAYSLIP (outside the transaction — if email fails, payment is
# still saved. We don't want a network error to roll back a real payment.) # still saved. We don't want a network error to roll back a real payment.)
# ========================================================================= # =========================================================================
# Lazy import — avoids crashing the app if xhtml2pdf isn't installed
from .utils import render_to_pdf
subject = f"Payslip for {worker.name} - {payroll_record.date}" subject = f"Payslip for {worker.name} - {payroll_record.date}"
# Context for both the HTML email body and the PDF attachment # Context for both the HTML email body and the PDF attachment
@ -827,7 +833,7 @@ def process_payment(request, worker_id):
html_message = render_to_string('core/email/payslip_email.html', email_context) html_message = render_to_string('core/email/payslip_email.html', email_context)
plain_message = strip_tags(html_message) plain_message = strip_tags(html_message)
# 2. Render PDF attachment # 2. Render PDF attachment (returns None if xhtml2pdf is not installed)
pdf_content = render_to_pdf('core/pdf/payslip_pdf.html', email_context) pdf_content = render_to_pdf('core/pdf/payslip_pdf.html', email_context)
# 3. Send email with PDF attached # 3. Send email with PDF attached