feat(phase-37): timer pause/resume + professional reporting engine
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Pause/Resume timer:
- Add paused_at, red_paused_seconds, blue_paused_seconds fields to Test model
- Add pause_timer/resume_timer workflow functions with accumulated pause tracking
- Auto-resume on phase submit; subtract paused time from worklog duration
- Add POST /tests/{id}/pause-timer and resume-timer endpoints
- Update LiveTimer component with pause/resume button and paused visual state
- Wire pause/resume mutations through TestDetailPage and TestDetailHeader
Professional Reporting Engine - Fase 2:
- Add ReportEngine service with Jinja2 HTML rendering, WeasyPrint PDF, and docxtpl DOCX
- Add corporate CSS stylesheet with cover page, data tables, stats grid, findings
- Create purple_campaign, coverage_report, and executive_summary HTML templates
- Add report_generation_service collecting domain data for each report type
- Add professional_reports router: GET /reports/generate/purple-campaign/{id}, coverage-summary, executive-summary
- Add analytics router with flat JSON endpoints for PowerBI: /coverage, /tests, /trends, /operators
- Add advanced_metrics router: /coverage-by-tactic, /never-tested, /avg-validation-time, /detection-rate-trend
- Add weasyprint and docxtpl to requirements.txt
- Add REPORT_TEMPLATES_DIR, REPORT_OUTPUT_DIR, COMPANY_NAME, COMPANY_LOGO_PATH to config
This commit is contained in:
93
backend/app/services/report_engine.py
Normal file
93
backend/app/services/report_engine.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Report engine — renders Jinja2 HTML templates to PDF, DOCX, and HTML.
|
||||
|
||||
Uses WeasyPrint for PDF generation and docxtpl for DOCX.
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReportEngine:
|
||||
"""Template-based report generator supporting PDF, DOCX, and HTML output."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.jinja_env = Environment(
|
||||
loader=FileSystemLoader(settings.REPORT_TEMPLATES_DIR),
|
||||
autoescape=True,
|
||||
)
|
||||
os.makedirs(settings.REPORT_OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
def render_html(self, template_name: str, context: dict) -> str:
|
||||
"""Render a Jinja2 template to an HTML string."""
|
||||
template = self.jinja_env.get_template(f"{template_name}.html")
|
||||
context.setdefault("company_name", settings.COMPANY_NAME)
|
||||
context.setdefault("generated_at", datetime.utcnow().strftime("%B %d, %Y %H:%M UTC"))
|
||||
return template.render(context)
|
||||
|
||||
def generate_pdf(self, template_name: str, context: dict) -> str:
|
||||
"""Render HTML and convert to PDF with WeasyPrint."""
|
||||
from weasyprint import HTML, CSS
|
||||
|
||||
html_content = self.render_html(template_name, context)
|
||||
css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css")
|
||||
output_path = os.path.join(
|
||||
settings.REPORT_OUTPUT_DIR,
|
||||
f"{template_name}_{uuid.uuid4().hex[:8]}.pdf",
|
||||
)
|
||||
|
||||
stylesheets = []
|
||||
if os.path.exists(css_path):
|
||||
stylesheets.append(CSS(filename=css_path))
|
||||
|
||||
HTML(
|
||||
string=html_content,
|
||||
base_url=settings.REPORT_TEMPLATES_DIR,
|
||||
).write_pdf(output_path, stylesheets=stylesheets)
|
||||
|
||||
logger.info("PDF generated: %s", output_path)
|
||||
return output_path
|
||||
|
||||
def generate_docx(self, template_name: str, context: dict) -> str:
|
||||
"""Render a .docx template with docxtpl."""
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
template_path = os.path.join(
|
||||
settings.REPORT_TEMPLATES_DIR, f"{template_name}.docx"
|
||||
)
|
||||
output_path = os.path.join(
|
||||
settings.REPORT_OUTPUT_DIR,
|
||||
f"{template_name}_{uuid.uuid4().hex[:8]}.docx",
|
||||
)
|
||||
|
||||
doc = DocxTemplate(template_path)
|
||||
context.setdefault("company_name", settings.COMPANY_NAME)
|
||||
context.setdefault("generated_at", datetime.utcnow().strftime("%B %d, %Y"))
|
||||
doc.render(context)
|
||||
doc.save(output_path)
|
||||
|
||||
logger.info("DOCX generated: %s", output_path)
|
||||
return output_path
|
||||
|
||||
def generate_html_file(self, template_name: str, context: dict) -> str:
|
||||
"""Render and save a standalone HTML report."""
|
||||
html_content = self.render_html(template_name, context)
|
||||
output_path = os.path.join(
|
||||
settings.REPORT_OUTPUT_DIR,
|
||||
f"{template_name}_{uuid.uuid4().hex[:8]}.html",
|
||||
)
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
|
||||
logger.info("HTML report generated: %s", output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
report_engine = ReportEngine()
|
||||
Reference in New Issue
Block a user