Created
January 2, 2026 16:23
-
-
Save pjaudiomv/a1e87fff10e165a2f5e98c5cd9af7d59 to your computer and use it in GitHub Desktop.
Generate a PDF report from audit_duplicate_keys_results.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Generate a PDF report from audit_duplicate_keys_results.json | |
| This script reads the JSON output from audit_duplicate_format_keys.py | |
| and generates a formatted PDF report. | |
| Usage: | |
| python3 generate_pdf_report.py # Use default input file | |
| python3 generate_pdf_report.py audit_duplicate_keys_results.json | |
| python3 generate_pdf_report.py input.json output.pdf | |
| Requirements: | |
| pip install reportlab | |
| """ | |
| import json | |
| import sys | |
| from datetime import datetime | |
| from typing import List, Dict | |
| 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 | |
| from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak | |
| from reportlab.platypus import KeepTogether | |
| from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT | |
| def load_results(filepath: str) -> List[Dict]: | |
| """Load audit results from JSON file.""" | |
| try: | |
| with open(filepath, 'r') as f: | |
| return json.load(f) | |
| except FileNotFoundError: | |
| print(f"❌ Error: File '{filepath}' not found", file=sys.stderr) | |
| sys.exit(1) | |
| except json.JSONDecodeError as e: | |
| print(f"❌ Error parsing JSON: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| def create_pdf_report(results: List[Dict], output_file: str): | |
| """Generate a PDF report from audit results.""" | |
| doc = SimpleDocTemplate( | |
| output_file, | |
| pagesize=letter, | |
| rightMargin=0.5*inch, | |
| leftMargin=0.5*inch, | |
| topMargin=0.75*inch, | |
| bottomMargin=0.75*inch | |
| ) | |
| # Container for the 'Flowable' objects | |
| elements = [] | |
| # Define styles | |
| styles = getSampleStyleSheet() | |
| title_style = ParagraphStyle( | |
| 'CustomTitle', | |
| parent=styles['Heading1'], | |
| fontSize=24, | |
| textColor=colors.HexColor('#2c3e50'), | |
| spaceAfter=30, | |
| alignment=TA_CENTER | |
| ) | |
| heading_style = ParagraphStyle( | |
| 'CustomHeading', | |
| parent=styles['Heading2'], | |
| fontSize=16, | |
| textColor=colors.HexColor('#34495e'), | |
| spaceAfter=12, | |
| spaceBefore=12 | |
| ) | |
| subheading_style = ParagraphStyle( | |
| 'CustomSubHeading', | |
| parent=styles['Heading3'], | |
| fontSize=12, | |
| textColor=colors.HexColor('#7f8c8d'), | |
| spaceAfter=8, | |
| spaceBefore=8 | |
| ) | |
| normal_style = styles['Normal'] | |
| # Title | |
| title = Paragraph("BMLT Duplicate Format Keys Audit Report", title_style) | |
| elements.append(title) | |
| # Report metadata | |
| report_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| metadata = Paragraph(f"<i>Generated: {report_date}</i>", normal_style) | |
| elements.append(metadata) | |
| elements.append(Spacer(1, 0.3*inch)) | |
| # Summary statistics | |
| total_servers = len(results) | |
| servers_with_duplicates = sum(1 for r in results if r.get('duplicates')) | |
| total_duplicate_groups = sum(len(r.get('duplicates', [])) for r in results) | |
| summary_heading = Paragraph("Summary", heading_style) | |
| elements.append(summary_heading) | |
| summary_data = [ | |
| ['Metric', 'Value'], | |
| ['Total Servers Audited', str(total_servers)], | |
| ['Servers with Duplicate Keys', str(servers_with_duplicates)], | |
| ['Total Duplicate Key Groups', str(total_duplicate_groups)] | |
| ] | |
| summary_table = Table(summary_data, colWidths=[3.5*inch, 2*inch]) | |
| summary_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3498db')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, 0), 12), | |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 12), | |
| ('BACKGROUND', (0, 1), (-1, -1), colors.beige), | |
| ('GRID', (0, 0), (-1, -1), 1, colors.grey), | |
| ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), | |
| ('FONTSIZE', (0, 1), (-1, -1), 10), | |
| ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#ecf0f1')]) | |
| ])) | |
| elements.append(summary_table) | |
| elements.append(Spacer(1, 0.4*inch)) | |
| # Recommendations section | |
| if total_duplicate_groups > 0: | |
| recommendations_heading = Paragraph("How to Fix Duplicate Format Keys", heading_style) | |
| elements.append(recommendations_heading) | |
| recommendations_text = """ | |
| <b>Why This Is a Problem:</b><br/> | |
| Duplicate format keys can cause confusion for users and client applications. When multiple formats | |
| share the same key_string in the same language, it becomes unclear which format is being referenced. | |
| This can lead to inconsistent meeting search results and display issues.<br/><br/> | |
| <b>Recommended Solutions:</b><br/> | |
| <br/> | |
| <b>1. Merge Duplicate Formats</b><br/> | |
| If the duplicate formats represent the same concept, merge them into a single format: | |
| <br/> | |
| • Choose one format to keep (usually the one used by more meetings)<br/> | |
| • Update all meetings using the other format(s) to use the kept format<br/> | |
| • Delete the redundant format(s)<br/> | |
| <br/> | |
| <b>2. Rename Format Keys</b><br/> | |
| If the formats represent different concepts, give them unique keys: | |
| <br/> | |
| • Review what each format represents<br/> | |
| • Assign a unique, descriptive key to each format<br/> | |
| • Keep the key short (1-3 characters) for compatibility<br/> | |
| <br/> | |
| <b>3. Use Database Updates</b><br/> | |
| For formats with many meetings, use SQL to bulk update:<br/> | |
| <br/> | |
| <font face="Courier" size="8"> | |
| -- Example: Merge format 36 into format 29<br/> | |
| UPDATE na_comdef_meetings_main SET format_shared_id_list = <br/> | |
| REPLACE(format_shared_id_list, ',36,', ',29,');<br/> | |
| DELETE FROM na_comdef_formats WHERE shared_id_bigint = 36;<br/> | |
| </font> | |
| <br/> | |
| <b>4. Test After Changes</b><br/> | |
| After making changes:<br/> | |
| • Clear any application caches<br/> | |
| • Verify meeting searches work correctly<br/> | |
| • Check that format displays are consistent<br/> | |
| • Re-run this audit to confirm duplicates are resolved<br/> | |
| """ | |
| recommendations_para = Paragraph(recommendations_text, normal_style) | |
| elements.append(recommendations_para) | |
| elements.append(Spacer(1, 0.4*inch)) | |
| # Detailed results | |
| if total_duplicate_groups > 0: | |
| details_heading = Paragraph("Detailed Results", heading_style) | |
| elements.append(details_heading) | |
| elements.append(Spacer(1, 0.2*inch)) | |
| for result in results: | |
| if not result.get('duplicates'): | |
| continue | |
| server_name = result.get('server', 'Unknown Server') | |
| server_id = result.get('server_id', 'N/A') | |
| server_url = result.get('url', 'N/A') | |
| # Server header | |
| server_info = f"<b>{server_name}</b> (ID: {server_id})" | |
| server_para = Paragraph(server_info, subheading_style) | |
| url_para = Paragraph(f"<i>{server_url}</i>", normal_style) | |
| server_elements = [server_para, url_para, Spacer(1, 0.1*inch)] | |
| # Process each duplicate group | |
| for dup in result['duplicates']: | |
| lang = dup.get('language', 'unknown') | |
| key = dup.get('key', 'unknown') | |
| formats = dup.get('formats', []) | |
| dup_header = Paragraph( | |
| f"<b>Language:</b> {lang} | <b>Key:</b> {key} | <b>Count:</b> {len(formats)}", | |
| normal_style | |
| ) | |
| server_elements.append(dup_header) | |
| server_elements.append(Spacer(1, 0.1*inch)) | |
| # Format details table | |
| format_data = [['Format ID', 'Name', 'Meetings Using This Format']] | |
| for fmt in formats: | |
| fmt_id = fmt.get('id', 'N/A') | |
| fmt_name = fmt.get('name_string', 'N/A') | |
| # Get meeting IDs for this format | |
| format_usage = result.get('format_usage', {}) | |
| meeting_ids = format_usage.get(fmt_id, []) | |
| if meeting_ids: | |
| # Sort and format meeting IDs | |
| sorted_meetings = sorted(meeting_ids) | |
| meeting_str = ', '.join(str(mid) for mid in sorted_meetings) | |
| # Wrap in Paragraph to enable text wrapping | |
| meeting_count = Paragraph( | |
| f"<b>{len(meeting_ids)} meeting(s):</b><br/>{meeting_str}", | |
| normal_style | |
| ) | |
| else: | |
| meeting_count = Paragraph("No meetings found", normal_style) | |
| # Wrap other cells in Paragraph too for consistency | |
| format_data.append([ | |
| Paragraph(str(fmt_id), normal_style), | |
| Paragraph(str(fmt_name), normal_style), | |
| meeting_count | |
| ]) | |
| format_table = Table(format_data, colWidths=[0.8*inch, 2.2*inch, 4*inch]) | |
| format_table.setStyle(TableStyle([ | |
| ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e74c3c')), | |
| ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), | |
| ('ALIGN', (0, 0), (-1, -1), 'LEFT'), | |
| ('VALIGN', (0, 0), (-1, -1), 'TOP'), | |
| ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), | |
| ('FONTSIZE', (0, 0), (-1, 0), 10), | |
| ('BOTTOMPADDING', (0, 0), (-1, 0), 8), | |
| ('TOPPADDING', (0, 1), (-1, -1), 6), | |
| ('BOTTOMPADDING', (0, 1), (-1, -1), 6), | |
| ('BACKGROUND', (0, 1), (-1, -1), colors.white), | |
| ('GRID', (0, 0), (-1, -1), 0.5, colors.grey), | |
| ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'), | |
| ('FONTSIZE', (0, 1), (-1, -1), 8), | |
| ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#fadbd8')]) | |
| ])) | |
| server_elements.append(format_table) | |
| server_elements.append(Spacer(1, 0.2*inch)) | |
| # Keep server results together when possible | |
| elements.append(KeepTogether(server_elements)) | |
| elements.append(Spacer(1, 0.3*inch)) | |
| else: | |
| no_issues = Paragraph( | |
| "<i>No duplicate format keys found in any server.</i>", | |
| normal_style | |
| ) | |
| elements.append(no_issues) | |
| # Build PDF | |
| doc.build(elements) | |
| print(f"✓ PDF report generated: {output_file}") | |
| def main(): | |
| """Main execution function.""" | |
| # Determine input and output files | |
| input_file = "audit_duplicate_keys_results.json" | |
| output_file = "audit_duplicate_keys_report.pdf" | |
| if len(sys.argv) > 1: | |
| input_file = sys.argv[1] | |
| if len(sys.argv) > 2: | |
| output_file = sys.argv[2] | |
| else: | |
| # Generate output filename from input filename | |
| if input_file.endswith('.json'): | |
| output_file = input_file[:-5] + '_report.pdf' | |
| print(f"Reading results from: {input_file}") | |
| results = load_results(input_file) | |
| print(f"Generating PDF report...") | |
| create_pdf_report(results, output_file) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment