class AuditPageGenerator
A class that generates comprehensive PDF audit trail pages for documents, including document information, reviews, approvals, revision history, and event history with electronic signatures.
/tf/active/vicechatdev/document_auditor/src/audit_page_generator.py
55 - 434
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 generatestatus: 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
osjsonloggingdatetimetempfilefitzreportlabdocument_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/
OptionalUsage 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
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class PDFGenerator 68.0% similar
-
class DocumentProcessor 61.3% similar
-
class ControlledDocumentConverter 59.5% similar
-
class DocumentMerger 59.2% similar
-
class HashGenerator 57.1% similar