From 74cd93fede41ab68f55710afe2fd7745ef9da6fc Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Sun, 22 Feb 2026 21:07:33 +0200 Subject: [PATCH] Fix 503: make xhtml2pdf import lazy to prevent app crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- core/utils.py | 20 +++++++++++++++++++- core/views.py | 10 ++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/core/utils.py b/core/utils.py index e6bdb5f..d6596cf 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,10 +1,17 @@ # === PDF GENERATION === # Converts a Django HTML template into a PDF file using xhtml2pdf. # 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 django.template.loader import get_template -from xhtml2pdf import pisa + +logger = logging.getLogger(__name__) 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: 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 template = get_template(template_src) html = template.render(context_dict) diff --git a/core/views.py b/core/views.py index 9720608..d9eb97d 100644 --- a/core/views.py +++ b/core/views.py @@ -22,7 +22,9 @@ from django.conf import settings from .models import Worker, Project, WorkLog, Team, PayrollRecord, Loan, PayrollAdjustment 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 === @@ -812,6 +814,10 @@ def process_payment(request, worker_id): # 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.) # ========================================================================= + + # 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}" # 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) 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) # 3. Send email with PDF attached