feat(pdf): improve download filename with company names and date

- Generate descriptive filenames: swot-{company}-vs-{competitor}-{date}.pdf
- Sanitize company names for filesystem safety
- Include date in YYYY-MM-DD format
- Handle multiple comparison entities (e.g., "plus2" for 2 additional)

Examples:
- Single entity: "swot-Apple-2026-02-04.pdf"
- With comparison: "swot-Apple-vs-Microsoft-2026-02-04.pdf"
- Multiple comparisons: "swot-Apple-vs-Microsoft-plus2-2026-02-04.pdf"

Fixes: Downloaded PDF had generic filename with .txt suffix

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 17:04:39 -05:00
parent a7fc31e555
commit dd2b2e8a33
2 changed files with 43 additions and 7 deletions

View File

@ -218,8 +218,35 @@ async def download_pdf(request: Request) -> StreamingResponse:
# Cache the generated PDF
pdf_cache.set(session_id, result, pdf_buffer)
# Prepare filename
filename = f"swot-analysis-{session_id[:8]}.pdf"
# Prepare filename with company names and date
import re
from datetime import datetime
# Sanitize company names for filename (remove special chars, limit length)
def sanitize_filename(text: str, max_length: int = 30) -> str:
# Remove special characters, keep alphanumeric, spaces, hyphens
text = re.sub(r"[^\w\s-]", "", text)
# Replace spaces with hyphens
text = re.sub(r"\s+", "-", text.strip())
# Limit length
return text[:max_length].rstrip("-")
# Build entity string
primary = sanitize_filename(result.primary_entity)
if result.comparison_entities:
# Include first comparison entity
comparison = sanitize_filename(result.comparison_entities[0])
entities_str = f"{primary}-vs-{comparison}"
if len(result.comparison_entities) > 1:
entities_str += f"-plus{len(result.comparison_entities) - 1}"
else:
entities_str = primary
# Add date
date_str = datetime.now().strftime("%Y-%m-%d")
# Build final filename
filename = f"swot-{entities_str}-{date_str}.pdf"
# Return as streaming response (iterate over BytesIO in chunks)
def iterfile():
@ -230,7 +257,7 @@ async def download_pdf(request: Request) -> StreamingResponse:
content=iterfile(),
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Content-Disposition": f'attachment; filename="{filename}"',
"Cache-Control": "no-cache",
},
)

View File

@ -230,7 +230,9 @@ class TestPDFDownloadEndpoint:
assert response.status_code == 200
assert response.headers["content-type"] == "application/pdf"
assert "attachment" in response.headers["content-disposition"]
assert "swot-analysis" in response.headers["content-disposition"]
# New filename format: swot-{company}-{date}.pdf
assert "swot-" in response.headers["content-disposition"]
assert ".pdf" in response.headers["content-disposition"]
# Verify it's a valid PDF by reading the stream
body_content = b""
@ -281,7 +283,9 @@ class TestPDFDownloadEndpoint:
async def test_download_pdf_filename_format(
self, mock_session_id: str, sample_swot_analysis
):
"""Test PDF filename follows expected format"""
"""Test PDF filename follows expected format: swot-{company}-{date}.pdf"""
from datetime import datetime
from backend.site.router import download_pdf
# Create mock request
@ -297,5 +301,10 @@ class TestPDFDownloadEndpoint:
assert response.status_code == 200
disposition = response.headers["content-disposition"]
# Format: swot-analysis-{session_id[:8]}.pdf
assert f"swot-analysis-{mock_session_id[:8]}.pdf" in disposition
# New format: swot-{primary_entity}-vs-{comparison[0]}-{date}.pdf
# Sample has primary_entity="Google", comparison_entities=["Microsoft", "Amazon"]
assert "swot-Google-vs-Microsoft" in disposition
assert "plus1" in disposition # +1 more comparison (Amazon)
assert datetime.now().strftime("%Y-%m-%d") in disposition
assert ".pdf" in disposition