🔍 Code Extractor

class AuditPageGenerator

Maturity: 47

A class that generates comprehensive PDF audit trail pages for documents, including document information, reviews, approvals, revision history, and event history with electronic signatures.

File:
/tf/active/vicechatdev/document_auditor/src/audit_page_generator.py
Lines:
55 - 434
Complexity:
complex

Purpose

The AuditPageGenerator class is responsible for creating professionally formatted PDF audit trail documents that track the complete lifecycle of a document. It handles document metadata, review cycles, approval workflows, revision tracking, and event logging. The class integrates with SignatureGenerator to embed electronic signatures for approved/rejected reviews and approvals. It uses ReportLab to create structured, styled PDF documents with tables, custom fonts (DejaVu Sans for Unicode support), and proper formatting for regulatory compliance and document management systems.

Source Code

class AuditPageGenerator:
    """Generates audit trail pages for documents"""
    
    def __init__(self):
        self.logger = logging.getLogger(__name__)
        self.signature_generator = SignatureGenerator()
        
        # Register fonts for better Unicode support
        try:
            # Try to register a good sans-serif font
            if not "DejaVuSans" in pdfmetrics.getRegisteredFontNames():
                font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
                if os.path.exists(font_path):
                    pdfmetrics.registerFont(TTFont("DejaVuSans", font_path))
                    pdfmetrics.registerFont(TTFont("DejaVuSans-Bold", 
                                               "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"))
        except Exception as e:
            self.logger.warning(f"Could not register custom fonts: {e}")

    def generate_signature_section(self, name, status, date_str):
        """
        Generate a consistently formatted signature section for reviews/approvals
        
        Args:
            name (str): Name of reviewer/approver
            status (str): Status (APPROVED/REJECTED)
            date_str (str): Date string
            
        Returns:
            list: List containing a properly sized signature image to add to tables
        """
        # Only generate signatures for completed reviews/approvals with APPROVED/REJECTED status
        if date_str and date_str != 'N/A' and status in ['APPROVED', 'REJECTED']:
            # Generate the signature image
            generated_sig_path = self.signature_generator.generate_signature_image(name, width=300, height=100)
            if generated_sig_path and os.path.exists(generated_sig_path):
                try:
                    # Create properly sized image - larger for better readability
                    img = Image(generated_sig_path, width=2.5*inch, height=1.0*inch)
                    return img
                except Exception as e:
                    self.logger.warning(f"Could not create signature image: {e}")
        
        # Return text for incomplete or failed signatures
        if date_str and date_str != 'N/A':
            return "Electronic Signature on File"
        else:
            return "Pending"
    
    def generate_audit_page(self, audit_data, output_path):
        """
        Generate PDF audit page from JSON data
        
        Args:
            audit_data (dict): Audit data as a dictionary
            output_path (str): Path where the audit PDF will be saved
            
        Returns:
            str: Path to the generated PDF
        """
        try:
            # Create the PDF
            doc = SimpleDocTemplate(
                output_path,
                pagesize=A4,
                leftMargin=1.5*cm,
                rightMargin=1.5*cm,
                topMargin=1.5*cm,
                bottomMargin=1.5*cm,
                title="Document Audit Trail"
            )
            
            # Create the styles
            styles = getSampleStyleSheet()
            
            # Add custom styles
            styles.add(ParagraphStyle(
                name='CustomHeading1',
                parent=styles['Heading1'],
                fontName='DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold',
                fontSize=14,
                spaceAfter=12,
                spaceBefore=6,
                textColor=colors.navy
            ))
            
            styles.add(ParagraphStyle(
                name='CustomHeading2',
                parent=styles['Heading2'],
                fontName='DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold',
                fontSize=12,
                spaceAfter=8,
                spaceBefore=6,
                textColor=colors.darkblue
            ))
            
            styles.add(ParagraphStyle(
                name='CustomNormal',
                parent=styles['Normal'],
                fontName='DejaVuSans' if 'DejaVuSans' in pdfmetrics.getRegisteredFontNames() else 'Helvetica',
                fontSize=10,
                spaceAfter=6
            ))
            
            styles.add(ParagraphStyle(
                name='TableHeader',
                parent=styles['Normal'],
                fontName='DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold',
                fontSize=9,
                alignment=1,  # Center alignment
                textColor=colors.white
            ))
            
            styles.add(ParagraphStyle(
                name='TableCell',
                parent=styles['Normal'],
                fontName='DejaVuSans' if 'DejaVuSans' in pdfmetrics.getRegisteredFontNames() else 'Helvetica',
                fontSize=9,
                alignment=0,  # Left alignment
                wordWrap='CJK'  # Better wrapping for all languages
            ))
            
            # Build the document content
            story = []
            
            # Add title
            story.append(Paragraph("Document Audit Trail", styles['CustomHeading1']))
            story.append(Spacer(1, 0.1*inch))
            
            # Add document information section
            story.append(Paragraph("Document Information", styles['CustomHeading2']))
            doc_info = [
                ["Document Title:", audit_data.get('document_title', 'N/A')],
                ["Document ID:", audit_data.get('document_id', 'N/A')],
                ["Version:", audit_data.get('version', 'N/A')],
                ["Author:", audit_data.get('author', 'N/A')],
                ["Department:", audit_data.get('department', 'N/A')],
                ["Creation Date:", audit_data.get('creation_date', 'N/A')]
            ]
            
            # Create document info table with proper styling
            doc_info_table = Table(doc_info, colWidths=[doc.width*0.25, doc.width*0.65])
            doc_info_table.setStyle(TableStyle([
                ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
                ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
                ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                ('FONTNAME', (0, 0), (0, -1), 'DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
                ('FONTNAME', (1, 0), (1, -1), 'DejaVuSans' if 'DejaVuSans' in pdfmetrics.getRegisteredFontNames() else 'Helvetica'),
                ('FONTSIZE', (0, 0), (-1, -1), 9),
                ('LEFTPADDING', (0, 0), (-1, -1), 6),
                ('RIGHTPADDING', (0, 0), (-1, -1), 6),
                ('TOPPADDING', (0, 0), (-1, -1), 4),
                ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
            ]))
            
            story.append(doc_info_table)
            story.append(Spacer(1, 0.2*inch))
            
            # Add reviews section if present
            if 'reviews' in audit_data and audit_data['reviews']:
                story.append(Paragraph("Document Reviews", styles['CustomHeading2']))
                
                # Process each review
                for i, review in enumerate(audit_data['reviews']):
                    # Format the review information
                    review_data = [
                        ["Reviewer:", review.get('reviewer_name', 'N/A')],
                        ["Role:", review.get('reviewer_role', 'N/A')],
                        ["Date:", review.get('review_date', 'N/A')],
                        ["Status:", review.get('status', 'N/A')],
                        ["Comments:", Paragraph(review.get('comments', 'N/A'), styles['TableCell'])]
                    ]
                    
                    # Add signature using the helper method
                    review_date = review.get('review_date', '')
                    reviewer_name = review.get('reviewer_name', 'Unknown Reviewer')
                    status = review.get('status', '')
                    
                    # Get signature from helper method
                    signature = self.generate_signature_section(reviewer_name, status, review_date)
                    
                    if isinstance(signature, Image):
                        review_data.append(["Signature:", signature])
                    else:
                        review_data.append(["Signature:", signature])
                    
                    # Create a table for this review
                    review_table = Table(review_data, colWidths=[doc.width*0.2, doc.width*0.7])
                    review_table.setStyle(TableStyle([
                        ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
                        ('BACKGROUND', (0, 0), (0, -1), colors.lightblue),
                        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                        ('FONTNAME', (0, 0), (0, -1), 'DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
                        ('FONTSIZE', (0, 0), (-1, -1), 9),
                        ('LEFTPADDING', (0, 0), (-1, -1), 6),
                        ('RIGHTPADDING', (0, 0), (-1, -1), 6),
                        ('TOPPADDING', (0, 0), (-1, -1), 4),
                        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
                        # Special handling for wrapped text in comments cell
                        ('VALIGN', (1, 4), (1, 4), 'TOP'),  # Comments row, align top
                        # Special handling for signature row - more space
                        ('TOPPADDING', (0, 5), (-1, 5), 8),
                        ('BOTTOMPADDING', (0, 5), (-1, 5), 8),
                    ]))
                    
                    story.append(review_table)
                    story.append(Spacer(1, 0.1*inch))
            
            # Add approvals section if present
            if 'approvals' in audit_data and audit_data['approvals']:
                story.append(Paragraph("Document Approvals", styles['CustomHeading2']))
                
                # Process each approval
                for i, approval in enumerate(audit_data['approvals']):
                    # Format the approval information
                    approval_data = [
                        ["Approver:", approval.get('approver_name', 'N/A')],
                        ["Role:", approval.get('approver_role', 'N/A')],
                        ["Date:", approval.get('approval_date', 'N/A')],
                        ["Status:", approval.get('status', 'N/A')],
                        ["Comments:", Paragraph(approval.get('comments', 'N/A'), styles['TableCell'])]
                    ]
                    
                    # Add signature using the helper method
                    approval_date = approval.get('approval_date', '')
                    approver_name = approval.get('approver_name', 'Unknown Approver')
                    status = approval.get('status', '')
                    
                    # Get signature from helper method
                    signature = self.generate_signature_section(approver_name, status, approval_date)
                    
                    if isinstance(signature, Image):
                        approval_data.append(["Signature:", signature])
                    else:
                        approval_data.append(["Signature:", signature])
                    
                    # Create a table for this approval
                    approval_table = Table(approval_data, colWidths=[doc.width*0.2, doc.width*0.7])
                    approval_table.setStyle(TableStyle([
                        ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
                        ('BACKGROUND', (0, 0), (0, -1), colors.lightgreen),
                        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                        ('FONTNAME', (0, 0), (0, -1), 'DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
                        ('FONTSIZE', (0, 0), (-1, -1), 9),
                        ('LEFTPADDING', (0, 0), (-1, -1), 6),
                        ('RIGHTPADDING', (0, 0), (-1, -1), 6),
                        ('TOPPADDING', (0, 0), (-1, -1), 4),
                        ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
                        # Special handling for wrapped text in comments cell
                        ('VALIGN', (1, 4), (1, 4), 'TOP'),  # Comments row, align top
                        # Special handling for signature row - more space
                        ('TOPPADDING', (0, 5), (-1, 5), 8),
                        ('BOTTOMPADDING', (0, 5), (-1, 5), 8),
                    ]))
                    
                    story.append(approval_table)
                    story.append(Spacer(1, 0.1*inch))
            
            # Add revision history if present
            if 'revision_history' in audit_data and audit_data['revision_history']:
                story.append(Paragraph("Revision History", styles['CustomHeading2']))
                
                # Create headers for revision history table
                revision_headers = ["Version", "Date", "Author", "Changes"]
                revision_data = [revision_headers]
                
                # Add each revision entry
                for revision in audit_data['revision_history']:
                    changes = revision.get('changes', 'N/A')
                    # Wrap long text in a Paragraph
                    changes_para = Paragraph(changes, styles['TableCell'])
                    
                    revision_data.append([
                        revision.get('version', 'N/A'),
                        revision.get('date', 'N/A'),
                        revision.get('author', 'N/A'),
                        changes_para
                    ])
                
                # Create the revision table with proper column widths
                col_widths = [doc.width*0.1, doc.width*0.2, doc.width*0.15, doc.width*0.45]
                revision_table = Table(revision_data, colWidths=col_widths, repeatRows=1)
                
                # Style the revision table
                table_style = [
                    # Grid and borders
                    ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
                    # Header row
                    ('BACKGROUND', (0, 0), (-1, 0), colors.navy),
                    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
                    ('FONTNAME', (0, 0), (-1, 0), 'DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
                    # Alignment
                    ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                    ('ALIGN', (0, 0), (-1, 0), 'CENTER'),  # Center-align header
                    # Special handling for the changes column
                    ('VALIGN', (3, 1), (3, -1), 'TOP'),  # Align changes text to top
                    # Padding
                    ('LEFTPADDING', (0, 0), (-1, -1), 4),
                    ('RIGHTPADDING', (0, 0), (-1, -1), 4),
                    ('TOPPADDING', (0, 0), (-1, -1), 4),
                    ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
                ]
                
                # Add zebra striping for better readability
                for i in range(1, len(revision_data)):
                    if i % 2 == 0:
                        table_style.append(('BACKGROUND', (0, i), (-1, i), colors.lightgrey))
                
                revision_table.setStyle(TableStyle(table_style))
                story.append(revision_table)
                story.append(Spacer(1, 0.1*inch))
            
            # Add event history if present
            if 'event_history' in audit_data and audit_data['event_history']:
                story.append(Paragraph("Event History", styles['CustomHeading2']))
                
                # Create headers for event history table
                event_headers = ["Date", "User", "Action", "Description", "Details"]
                event_data = [event_headers]
                
                # Add each event entry
                for event in audit_data['event_history']:
                    # Wrap long text in Paragraphs for better formatting
                    description = Paragraph(event.get('description', 'N/A'), styles['TableCell'])
                    details = Paragraph(event.get('details', 'N/A'), styles['TableCell'])
                    
                    event_data.append([
                        event.get('date', 'N/A'),
                        event.get('user', 'N/A'),
                        event.get('action', 'N/A'),
                        description,
                        details
                    ])
                
                # Create the event table with proper column widths
                col_widths = [doc.width*0.15, doc.width*0.15, doc.width*0.15, doc.width*0.2, doc.width*0.25]
                event_table = Table(event_data, colWidths=col_widths, repeatRows=1)
                
                # Style the event table
                table_style = [
                    # Grid and borders
                    ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
                    # Header row
                    ('BACKGROUND', (0, 0), (-1, 0), colors.navy),
                    ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
                    ('FONTNAME', (0, 0), (-1, 0), 'DejaVuSans-Bold' if 'DejaVuSans-Bold' in pdfmetrics.getRegisteredFontNames() else 'Helvetica-Bold'),
                    # Alignment
                    ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
                    ('ALIGN', (0, 0), (-1, 0), 'CENTER'),  # Center-align header
                    # Special handling for the description and details columns
                    ('VALIGN', (3, 1), (4, -1), 'TOP'),  # Align text to top
                    # Padding
                    ('LEFTPADDING', (0, 0), (-1, -1), 4),
                    ('RIGHTPADDING', (0, 0), (-1, -1), 4),
                    ('TOPPADDING', (0, 0), (-1, -1), 4),
                    ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
                ]
                
                # Add zebra striping for better readability
                for i in range(1, len(event_data)):
                    if i % 2 == 0:
                        table_style.append(('BACKGROUND', (0, i), (-1, i), colors.lightgrey))
                
                event_table.setStyle(TableStyle(table_style))
                story.append(event_table)
                story.append(Spacer(1, 0.1*inch))
            
            # Add audit timestamp
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            story.append(Paragraph(f"Audit trail generated on: {timestamp}", styles['CustomNormal']))
            
            # Create the PDF
            doc.build(story)
            
            self.logger.info(f"Generated audit page: {output_path}")
            return output_path
            
        except Exception as e:
            self.logger.error(f"Error generating audit page: {e}")
            raise

Parameters

Name Type Default Kind
bases - -

Parameter Details

No constructor parameters: The __init__ method takes no parameters. It initializes a logger, creates a SignatureGenerator instance, and attempts to register DejaVu Sans fonts for better Unicode support.

Return Value

Instantiation returns an AuditPageGenerator object. The main method generate_audit_page() returns a string containing the path to the generated PDF file. The generate_signature_section() method returns either an Image object (for completed signatures), a string 'Electronic Signature on File', or 'Pending' depending on the status.

Class Interface

Methods

__init__(self)

Purpose: Initializes the AuditPageGenerator with a logger, SignatureGenerator instance, and attempts to register DejaVu Sans fonts for Unicode support

Returns: None

generate_signature_section(self, name: str, status: str, date_str: str) -> Union[Image, str]

Purpose: Generates a consistently formatted signature section for reviews/approvals, returning either an Image object with the signature or a status string

Parameters:

  • name: Name of the reviewer or approver whose signature to generate
  • status: Status of the review/approval (must be 'APPROVED' or 'REJECTED' for signature generation)
  • date_str: Date string indicating when the review/approval occurred (must not be 'N/A' or empty for signature generation)

Returns: Returns an Image object (2.5x1.0 inches) if signature is generated successfully, 'Electronic Signature on File' string if date is valid but signature generation fails, or 'Pending' if date is not available

generate_audit_page(self, audit_data: dict, output_path: str) -> str

Purpose: Generates a complete PDF audit trail page from structured audit data, including document information, reviews, approvals, revision history, and event history

Parameters:

  • audit_data: Dictionary containing audit trail data with keys: document_title, document_id, version, author, department, creation_date, reviews (list), approvals (list), revision_history (list), event_history (list)
  • output_path: File system path where the generated PDF will be saved

Returns: Returns the output_path string if successful, raises an exception if PDF generation fails

Attributes

Name Type Description Scope
logger logging.Logger Logger instance for logging warnings, errors, and info messages during audit page generation instance
signature_generator SignatureGenerator Instance of SignatureGenerator used to create electronic signature images for approved/rejected reviews and approvals instance

Dependencies

  • os
  • json
  • logging
  • datetime
  • tempfile
  • fitz
  • reportlab
  • document_auditor.src.utils.signature_generator

Required Imports

import os
import json
import logging
from datetime import datetime
import tempfile
import fitz
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter, A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak, ListFlowable, ListItem, Flowable
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from document_auditor.src.utils.signature_generator import SignatureGenerator
from reportlab.graphics import renderPDF
from reportlab.graphics.shapes import Drawing, Line
import random

Conditional/Optional Imports

These imports are only needed under specific conditions:

from reportlab.pdfbase.ttfonts import TTFont

Condition: only if custom fonts (DejaVu Sans) are available on the system at /usr/share/fonts/truetype/dejavu/

Optional

Usage Example

from document_auditor.src.utils.audit_page_generator import AuditPageGenerator

# Instantiate the generator
audit_gen = AuditPageGenerator()

# Prepare audit data
audit_data = {
    'document_title': 'Quality Management Plan',
    'document_id': 'QMP-001',
    'version': '2.1',
    'author': 'John Doe',
    'department': 'Quality Assurance',
    'creation_date': '2024-01-15',
    'reviews': [
        {
            'reviewer_name': 'Jane Smith',
            'reviewer_role': 'QA Manager',
            'review_date': '2024-01-20',
            'status': 'APPROVED',
            'comments': 'Document meets all quality standards'
        }
    ],
    'approvals': [
        {
            'approver_name': 'Bob Johnson',
            'approver_role': 'Director',
            'approval_date': '2024-01-22',
            'status': 'APPROVED',
            'comments': 'Approved for implementation'
        }
    ],
    'revision_history': [
        {
            'version': '2.1',
            'date': '2024-01-15',
            'author': 'John Doe',
            'changes': 'Updated compliance requirements'
        }
    ],
    'event_history': [
        {
            'date': '2024-01-15 10:30:00',
            'user': 'John Doe',
            'action': 'CREATED',
            'description': 'Document created',
            'details': 'Initial version created'
        }
    ]
}

# Generate the audit page
output_path = '/path/to/output/audit_trail.pdf'
pdf_path = audit_gen.generate_audit_page(audit_data, output_path)
print(f'Audit trail generated: {pdf_path}')

Best Practices

  • Always ensure the output directory exists and has write permissions before calling generate_audit_page()
  • The audit_data dictionary should contain all expected keys (document_title, document_id, version, etc.) to avoid 'N/A' placeholders
  • For signatures to be generated, reviews/approvals must have status 'APPROVED' or 'REJECTED' and a valid date
  • The class handles font registration failures gracefully, falling back to Helvetica if DejaVu Sans is unavailable
  • Long text in comments, changes, and details fields is automatically wrapped using Paragraph objects
  • The generated PDF uses A4 page size with 1.5cm margins on all sides
  • Instantiate once and reuse the same AuditPageGenerator object for multiple audit page generations
  • The class logs warnings and errors using Python's logging module, so configure logging appropriately
  • Signature images are generated at 2.5x1.0 inches for readability
  • The class creates temporary signature image files that are managed by the SignatureGenerator
  • Tables use zebra striping (alternating row colors) for better readability in revision and event history sections

Similar Components

AI-powered semantic similarity - components with related functionality:

  • class PDFGenerator 68.0% similar

    PDF document generation for reports and controlled documents This class provides methods to generate PDF documents from scratch, including audit reports, document covers, and certificate pages.

    From: /tf/active/vicechatdev/CDocs/utils/pdf_utils.py
  • class DocumentProcessor 61.3% similar

    A comprehensive document processing class that converts documents to PDF, adds audit trails, applies security features (watermarks, signatures, hashing), and optionally converts to PDF/A format with document protection.

    From: /tf/active/vicechatdev/document_auditor/src/document_processor.py
  • class ControlledDocumentConverter 59.5% similar

    A comprehensive document converter class that transforms controlled documents into archived PDFs with signature pages, audit trails, hash-based integrity verification, and PDF/A compliance for long-term archival.

    From: /tf/active/vicechatdev/CDocs/utils/document_converter.py
  • class DocumentMerger 59.2% similar

    A class that merges PDF documents with audit trail pages, combining an original PDF with an audit page and updating metadata to reflect the audit process.

    From: /tf/active/vicechatdev/document_auditor/src/document_merger.py
  • class HashGenerator 57.1% similar

    A class that provides cryptographic hashing functionality for PDF documents, including hash generation, embedding, and verification for document integrity checking.

    From: /tf/active/vicechatdev/document_auditor/src/security/hash_generator.py
← Back to Browse