| """
|
| export_formats.py
|
| Export grant proposals to DOCX and PDF formats
|
| """
|
|
|
| from docx import Document
|
| from docx.shared import Inches, Pt, RGBColor
|
| from docx.enum.text import WD_ALIGN_PARAGRAPH
|
| from docx.enum.style import WD_STYLE_TYPE
|
| import os
|
| from typing import Dict
|
| from datetime import datetime
|
|
|
| class ProposalExporter:
|
| """
|
| Export grant proposals to professional document formats
|
|
|
| Supports:
|
| - Microsoft Word (.docx)
|
| - PDF (via docx2pdf or reportlab)
|
| - HTML
|
| - Markdown
|
| """
|
|
|
| def __init__(self):
|
| self.doc = None
|
|
|
| def export_to_docx(
|
| self,
|
| proposal_data: Dict,
|
| output_path: str = "grant_proposal.docx",
|
| template: str = "nsf"
|
| ) -> str:
|
| """
|
| Export to Microsoft Word format
|
|
|
| Args:
|
| proposal_data: Complete proposal data from Phase 3
|
| output_path: Path for output file
|
| template: Template style ('nsf', 'nih', 'simple')
|
|
|
| Returns:
|
| Path to created file
|
| """
|
| print(f"π Exporting to DOCX: {output_path}")
|
|
|
|
|
| self.doc = Document()
|
|
|
|
|
| self._setup_styles()
|
|
|
|
|
| if template == "nsf":
|
| self._format_nsf_proposal(proposal_data)
|
| elif template == "nih":
|
| self._format_nih_proposal(proposal_data)
|
| else:
|
| self._format_simple_proposal(proposal_data)
|
|
|
|
|
| self.doc.save(output_path)
|
| print(f"β
DOCX saved: {output_path}")
|
|
|
| return output_path
|
|
|
| def _setup_styles(self):
|
| """Set up document styles"""
|
| styles = self.doc.styles
|
|
|
|
|
| if 'CustomTitle' not in [s.name for s in styles]:
|
| title_style = styles.add_style('CustomTitle', WD_STYLE_TYPE.PARAGRAPH)
|
| title_font = title_style.font
|
| title_font.name = 'Arial'
|
| title_font.size = Pt(18)
|
| title_font.bold = True
|
| title_font.color.rgb = RGBColor(0, 0, 128)
|
|
|
|
|
| heading1 = styles['Heading 1']
|
| heading1_font = heading1.font
|
| heading1_font.name = 'Arial'
|
| heading1_font.size = Pt(14)
|
| heading1_font.bold = True
|
| heading1_font.color.rgb = RGBColor(0, 0, 0)
|
|
|
|
|
| normal = styles['Normal']
|
| normal_font = normal.font
|
| normal_font.name = 'Times New Roman'
|
| normal_font.size = Pt(12)
|
|
|
| def _format_nsf_proposal(self, data: Dict):
|
| """Format as NSF-style proposal"""
|
|
|
| analysis = data.get('analysis', {})
|
| evaluation = data.get('evaluation', {})
|
| innovations = data.get('innovations', {})
|
| proposal = data.get('proposal', {}).get('proposal', {})
|
|
|
|
|
| self._add_cover_page(analysis, "NSF")
|
|
|
|
|
| self._add_section("EXECUTIVE SUMMARY", proposal.get('executive_summary', ''))
|
|
|
|
|
| self._add_section("PROJECT DESCRIPTION", proposal.get('project_description', ''))
|
|
|
|
|
| self._add_section("RESEARCH PLAN", proposal.get('research_plan', ''))
|
|
|
|
|
| self._add_section("BROADER IMPACTS", proposal.get('broader_impacts', ''))
|
|
|
|
|
| self._add_section("BUDGET JUSTIFICATION", proposal.get('budget_justification', ''))
|
|
|
|
|
| self._add_timeline_section(proposal.get('timeline', {}))
|
|
|
|
|
| self._add_section("REFERENCES", proposal.get('references', ''))
|
|
|
|
|
| self.doc.add_page_break()
|
| self._add_appendix(analysis, evaluation, innovations)
|
|
|
|
|
| def _format_nih_proposal(self, data: Dict):
|
| """Format as NIH-style proposal"""
|
|
|
| self._format_nsf_proposal(data)
|
|
|
| def _format_simple_proposal(self, data: Dict):
|
| """Format as simple proposal"""
|
|
|
| analysis = data.get('analysis', {})
|
| proposal = data.get('proposal', {}).get('proposal', {})
|
|
|
|
|
| title = self.doc.add_paragraph(analysis.get('title', 'Research Proposal'))
|
| title.style = 'CustomTitle'
|
| title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
|
| self.doc.add_paragraph()
|
|
|
|
|
| for section_name, section_content in proposal.items():
|
| if section_name == 'timeline':
|
| self._add_timeline_section(section_content)
|
| else:
|
| self._add_section(section_name.replace('_', ' ').upper(), str(section_content))
|
|
|
| def _add_cover_page(self, analysis: Dict, agency: str):
|
| """Add cover page"""
|
|
|
|
|
| title = self.doc.add_paragraph(f"GRANT PROPOSAL\n{agency}")
|
| title.style = 'CustomTitle'
|
| title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
|
| self.doc.add_paragraph()
|
|
|
|
|
| project_title = self.doc.add_heading(
|
| f"Extension and Application of:\n{analysis.get('title', 'Unknown')}",
|
| level=2
|
| )
|
| project_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
|
| self.doc.add_paragraph()
|
|
|
|
|
| table = self.doc.add_table(rows=6, cols=2)
|
| table.style = 'Light Grid Accent 1'
|
|
|
| rows_data = [
|
| ('Principal Investigator:', '[PI Name]'),
|
| ('Institution:', '[Institution Name]'),
|
| ('Duration:', '3 years'),
|
| ('Requested Amount:', '$500,000'),
|
| ('Submission Date:', datetime.now().strftime('%B %d, %Y')),
|
| ('Based on:', ', '.join(analysis.get('authors', ['Unknown'])[:3]))
|
| ]
|
|
|
| for i, (label, value) in enumerate(rows_data):
|
| table.rows[i].cells[0].text = label
|
| table.rows[i].cells[1].text = value
|
|
|
| self.doc.add_page_break()
|
|
|
| def _add_section(self, title: str, content: str):
|
| """Add a section to document"""
|
|
|
|
|
| self.doc.add_heading(title, level=1)
|
|
|
|
|
| paragraphs = content.split('\n\n')
|
| for para_text in paragraphs:
|
| if para_text.strip():
|
| self.doc.add_paragraph(para_text.strip())
|
|
|
| self.doc.add_paragraph()
|
|
|
| def _add_timeline_section(self, timeline: Dict):
|
| """Add timeline section with table"""
|
|
|
| self.doc.add_heading("PROJECT TIMELINE", level=1)
|
|
|
| if not timeline:
|
| self.doc.add_paragraph("Timeline to be determined.")
|
| return
|
|
|
|
|
| num_years = len(timeline)
|
| table = self.doc.add_table(rows=num_years + 1, cols=2)
|
| table.style = 'Light Grid Accent 1'
|
|
|
|
|
| table.rows[0].cells[0].text = "Period"
|
| table.rows[0].cells[1].text = "Milestones"
|
|
|
|
|
| for cell in table.rows[0].cells:
|
| for paragraph in cell.paragraphs:
|
| for run in paragraph.runs:
|
| run.font.bold = True
|
|
|
|
|
| for i, (year, milestones) in enumerate(timeline.items(), 1):
|
| table.rows[i].cells[0].text = year
|
|
|
| if isinstance(milestones, list):
|
| milestone_text = '\n'.join(f"β’ {m}" for m in milestones)
|
| else:
|
| milestone_text = str(milestones)
|
|
|
| table.rows[i].cells[1].text = milestone_text
|
|
|
| self.doc.add_paragraph()
|
|
|
|
|
| def _add_appendix(self, analysis: Dict, evaluation: Dict, innovations: Dict):
|
| """Add appendix with analysis details"""
|
|
|
| self.doc.add_heading("APPENDIX: DETAILED ANALYSIS", level=1)
|
|
|
|
|
| self.doc.add_heading("Paper Information", level=2)
|
| info_para = self.doc.add_paragraph()
|
| info_para.add_run(f"Title: ").bold = True
|
| info_para.add_run(f"{analysis.get('title', 'Unknown')}\n")
|
| info_para.add_run(f"Authors: ").bold = True
|
| info_para.add_run(f"{', '.join(analysis.get('authors', ['Unknown'])[:5])}\n")
|
| if analysis.get('year'):
|
| info_para.add_run(f"Year: ").bold = True
|
| info_para.add_run(f"{analysis['year']}\n")
|
|
|
|
|
| self.doc.add_heading("Quality Assessment Scores", level=2)
|
|
|
| scores = evaluation.get('scores', {})
|
| table = self.doc.add_table(rows=len(scores) + 1, cols=2)
|
| table.style = 'Light Grid Accent 1'
|
|
|
| table.rows[0].cells[0].text = "Metric"
|
| table.rows[0].cells[1].text = "Score"
|
|
|
| for i, (metric, score) in enumerate(scores.items(), 1):
|
| table.rows[i].cells[0].text = metric.capitalize()
|
| table.rows[i].cells[1].text = f"{score}/10"
|
|
|
| self.doc.add_paragraph()
|
|
|
|
|
| self.doc.add_heading("Key Contributions", level=2)
|
| for contrib in analysis.get('key_contributions', [])[:5]:
|
| self.doc.add_paragraph(contrib, style='List Bullet')
|
|
|
|
|
| self.doc.add_heading("Future Research Directions", level=2)
|
| for direction in innovations.get('future_directions', [])[:5]:
|
| para = self.doc.add_paragraph(style='List Bullet')
|
| para.add_run(direction.get('direction', 'N/A')).bold = True
|
| para.add_run(f"\n{direction.get('description', '')}")
|
|
|
| def export_to_pdf(
|
| self,
|
| proposal_data: Dict,
|
| output_path: str = "grant_proposal.pdf",
|
| method: str = "docx2pdf"
|
| ) -> str:
|
| """
|
| Export to PDF format
|
|
|
| Args:
|
| proposal_data: Complete proposal data
|
| output_path: Path for output file
|
| method: 'docx2pdf' or 'reportlab'
|
|
|
| Returns:
|
| Path to created file
|
| """
|
| print(f"π Exporting to PDF: {output_path}")
|
|
|
| if method == "docx2pdf":
|
|
|
| docx_path = output_path.replace('.pdf', '.docx')
|
| self.export_to_docx(proposal_data, docx_path)
|
|
|
|
|
| try:
|
| from docx2pdf import convert
|
| convert(docx_path, output_path)
|
| print(f"β
PDF saved: {output_path}")
|
| return output_path
|
| except ImportError:
|
| print("β οΈ docx2pdf not installed. Install with: pip install docx2pdf")
|
| print(" Returning DOCX file instead.")
|
| return docx_path
|
|
|
| elif method == "reportlab":
|
|
|
| return self._export_pdf_reportlab(proposal_data, output_path)
|
|
|
| else:
|
| raise ValueError(f"Unknown method: {method}")
|
|
|
| def _export_pdf_reportlab(self, proposal_data: Dict, output_path: str) -> str:
|
| """Export using reportlab (direct PDF)"""
|
|
|
| try:
|
| from reportlab.lib.pagesizes import letter
|
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| from reportlab.lib.units import inch
|
| from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
|
|
|
|
|
| doc = SimpleDocTemplate(output_path, pagesize=letter)
|
| story = []
|
| styles = getSampleStyleSheet()
|
|
|
|
|
| title_style = ParagraphStyle(
|
| 'CustomTitle',
|
| parent=styles['Heading1'],
|
| fontSize=18,
|
| textColor='navy',
|
| spaceAfter=30,
|
| alignment=TA_CENTER
|
| )
|
|
|
|
|
| analysis = proposal_data.get('analysis', {})
|
| proposal = proposal_data.get('proposal', {}).get('proposal', {})
|
|
|
|
|
| story.append(Paragraph("GRANT PROPOSAL", title_style))
|
| story.append(Paragraph(analysis.get('title', 'Unknown'), styles['Heading2']))
|
| story.append(Spacer(1, 0.5*inch))
|
|
|
|
|
| for section_name, section_content in proposal.items():
|
| if section_name != 'timeline':
|
| story.append(Paragraph(section_name.replace('_', ' ').upper(), styles['Heading1']))
|
|
|
|
|
| paragraphs = str(section_content).split('\n\n')
|
| for para_text in paragraphs:
|
| if para_text.strip():
|
| story.append(Paragraph(para_text.strip(), styles['BodyText']))
|
| story.append(Spacer(1, 0.2*inch))
|
|
|
| story.append(Spacer(1, 0.3*inch))
|
|
|
|
|
| doc.build(story)
|
| print(f"β
PDF saved: {output_path}")
|
| return output_path
|
|
|
| except ImportError:
|
| print("β οΈ reportlab not installed. Install with: pip install reportlab")
|
| return self.export_to_docx(proposal_data, output_path.replace('.pdf', '.docx'))
|
|
|
| def export_to_html(self, proposal_data: Dict, output_path: str = "grant_proposal.html") -> str:
|
| """Export to HTML format"""
|
|
|
| print(f"π Exporting to HTML: {output_path}")
|
|
|
| analysis = proposal_data.get('analysis', {})
|
| evaluation = proposal_data.get('evaluation', {})
|
| innovations = proposal_data.get('innovations', {})
|
| proposal = proposal_data.get('proposal', {}).get('proposal', {})
|
|
|
| html = f"""<!DOCTYPE html>
|
| <html>
|
| <head>
|
| <meta charset="UTF-8">
|
| <title>Grant Proposal - {analysis.get('title', 'Unknown')}</title>
|
| <style>
|
| body {{
|
| font-family: 'Times New Roman', serif;
|
| max-width: 800px;
|
| margin: 40px auto;
|
| padding: 20px;
|
| line-height: 1.6;
|
| }}
|
| h1 {{
|
| color: #003366;
|
| border-bottom: 2px solid #003366;
|
| padding-bottom: 10px;
|
| }}
|
| h2 {{
|
| color: #0066CC;
|
| margin-top: 30px;
|
| }}
|
| .cover {{
|
| text-align: center;
|
| margin-bottom: 50px;
|
| padding: 40px;
|
| background: #f5f5f5;
|
| border-radius: 10px;
|
| }}
|
| .section {{
|
| margin-bottom: 30px;
|
| }}
|
| table {{
|
| width: 100%;
|
| border-collapse: collapse;
|
| margin: 20px 0;
|
| }}
|
| th, td {{
|
| padding: 10px;
|
| border: 1px solid #ddd;
|
| text-align: left;
|
| }}
|
| th {{
|
| background-color: #003366;
|
| color: white;
|
| }}
|
| </style>
|
| </head>
|
| <body>
|
| <div class="cover">
|
| <h1>GRANT PROPOSAL</h1>
|
| <h2>{analysis.get('title', 'Unknown')}</h2>
|
| <p><strong>Based on work by:</strong> {', '.join(analysis.get('authors', ['Unknown'])[:5])}</p>
|
| <p><strong>Requested Amount:</strong> $500,000 over 3 years</p>
|
| </div>
|
| """
|
|
|
|
|
| for section_name, section_content in proposal.items():
|
| if section_name != 'timeline':
|
| html += f"""
|
| <div class="section">
|
| <h1>{section_name.replace('_', ' ').upper()}</h1>
|
| <p>{str(section_content).replace(chr(10), '</p><p>')}</p>
|
| </div>
|
| """
|
|
|
|
|
| if 'timeline' in proposal:
|
| html += """
|
| <div class="section">
|
| <h1>PROJECT TIMELINE</h1>
|
| <table>
|
| <tr>
|
| <th>Period</th>
|
| <th>Milestones</th>
|
| </tr>
|
| """
|
| for year, milestones in proposal['timeline'].items():
|
| milestone_html = '<br>'.join(f"β’ {m}" for m in milestones) if isinstance(milestones, list) else str(milestones)
|
| html += f"""
|
| <tr>
|
| <td><strong>{year}</strong></td>
|
| <td>{milestone_html}</td>
|
| </tr>
|
| """
|
| html += """
|
| </table>
|
| </div>
|
| """
|
|
|
| html += """
|
| </body>
|
| </html>
|
| """
|
|
|
| with open(output_path, 'w', encoding='utf-8') as f:
|
| f.write(html)
|
|
|
| print(f"β
HTML saved: {output_path}")
|
| return output_path
|
|
|
| def export_to_markdown(self, proposal_data: Dict, output_path: str = "grant_proposal.md") -> str:
|
| """Export to Markdown format"""
|
|
|
| print(f"π Exporting to Markdown: {output_path}")
|
|
|
| analysis = proposal_data.get('analysis', {})
|
| proposal = proposal_data.get('proposal', {}).get('proposal', {})
|
|
|
| md = f"""# Grant Proposal
|
|
|
| ## {analysis.get('title', 'Unknown')}
|
|
|
| **Based on work by:** {', '.join(analysis.get('authors', ['Unknown'])[:5])}
|
| **Requested Amount:** $500,000 over 3 years
|
| **Date:** {datetime.now().strftime('%B %d, %Y')}
|
|
|
| ---
|
|
|
| """
|
|
|
|
|
| for section_name, section_content in proposal.items():
|
| if section_name != 'timeline':
|
| md += f"## {section_name.replace('_', ' ').upper()}\n\n"
|
| md += f"{section_content}\n\n"
|
| md += "---\n\n"
|
|
|
|
|
| if 'timeline' in proposal:
|
| md += "## PROJECT TIMELINE\n\n"
|
| for year, milestones in proposal['timeline'].items():
|
| md += f"### {year}\n\n"
|
| if isinstance(milestones, list):
|
| for milestone in milestones:
|
| md += f"- {milestone}\n"
|
| else:
|
| md += f"{milestones}\n"
|
| md += "\n"
|
|
|
| with open(output_path, 'w', encoding='utf-8') as f:
|
| f.write(md)
|
|
|
| print(f"β
Markdown saved: {output_path}")
|
| return output_path
|
|
|
|
|
|
|
|
|
| def main():
|
| """Command-line interface"""
|
| import argparse
|
| import json
|
|
|
| parser = argparse.ArgumentParser(description="Export grant proposals to various formats")
|
| parser.add_argument("input_json", help="Input JSON file with proposal data")
|
| parser.add_argument("-f", "--format", choices=['docx', 'pdf', 'html', 'md', 'all'], default='docx', help="Output format")
|
| parser.add_argument("-o", "--output", help="Output filename (without extension)")
|
| parser.add_argument("-t", "--template", choices=['nsf', 'nih', 'simple'], default='nsf', help="Template style")
|
|
|
| args = parser.parse_args()
|
|
|
|
|
| with open(args.input_json, 'r') as f:
|
| data = json.load(f)
|
|
|
|
|
| if args.output:
|
| base_name = args.output
|
| else:
|
| base_name = "grant_proposal"
|
|
|
|
|
| exporter = ProposalExporter()
|
|
|
|
|
| if args.format == 'all':
|
| exporter.export_to_docx(data, f"{base_name}.docx", args.template)
|
| exporter.export_to_pdf(data, f"{base_name}.pdf")
|
| exporter.export_to_html(data, f"{base_name}.html")
|
| exporter.export_to_markdown(data, f"{base_name}.md")
|
| elif args.format == 'docx':
|
| exporter.export_to_docx(data, f"{base_name}.docx", args.template)
|
| elif args.format == 'pdf':
|
| exporter.export_to_pdf(data, f"{base_name}.pdf")
|
| elif args.format == 'html':
|
| exporter.export_to_html(data, f"{base_name}.html")
|
| elif args.format == 'md':
|
| exporter.export_to_markdown(data, f"{base_name}.md")
|
|
|
|
|
| if __name__ == "__main__":
|
| main() |