Files
jobs/web/email_templates.py
zwitschi 2185a07ff0
Some checks failed
CI/CD Pipeline / test (push) Failing after 4m9s
feat: Implement email sending utilities and templates for job notifications
- Added email_service.py for sending emails with SMTP configuration.
- Introduced email_templates.py to render job alert email subjects and bodies.
- Enhanced scraper.py to extract contact information from job listings.
- Updated settings.js to handle negative keyword input validation.
- Created email.html and email_templates.html for managing email subscriptions and templates in the admin interface.
- Modified base.html to include links for email alerts and templates.
- Expanded user settings.html to allow management of negative keywords.
- Updated utils.py to include functions for retrieving negative keywords and email settings.
- Enhanced job filtering logic to exclude jobs containing negative keywords.
2025-11-28 18:15:08 +01:00

107 lines
3.4 KiB
Python

"""Email templates for job notifications."""
from __future__ import annotations
from datetime import datetime, UTC
from typing import Iterable, Mapping, Dict, Any
DEFAULT_DATETIME_FORMAT = "%Y-%m-%d %H:%M"
DEFAULT_JOB_ALERT_SUBJECT = "{count_label}{scope}"
DEFAULT_JOB_ALERT_BODY = (
"Hi,\n\n{intro_line}{jobs_section}\n\nGenerated at {timestamp} UTC.\n"
"You are receiving this message because job alerts are enabled.\n"
)
class _SafeDict(dict):
def __missing__(self, key: str) -> str:
return ""
def _format_template(template: str, context: Dict[str, Any]) -> str:
safe_context = _SafeDict(
{k: ("\n".join(str(v) for v in context[k]) if isinstance(
context[k], list) else context[k]) for k in context}
)
return template.format_map(safe_context)
def render_job_alert_email(
jobs: Iterable[Mapping[str, object]],
*,
region: str | None = None,
keyword: str | None = None,
generated_at: datetime | None = None,
template_override: Mapping[str, str] | None = None,
) -> dict[str, Any]:
"""Render the subject/body for a job alert email.
Returns a dict with subject/body strings and the context used to render them.
"""
job_list = list(jobs)
generated_at = generated_at or datetime.now(UTC)
timestamp = generated_at.strftime(DEFAULT_DATETIME_FORMAT)
scope_parts = []
if region:
scope_parts.append(f"region: {region}")
if keyword:
scope_parts.append(f"keyword: {keyword}")
scope = " (" + ", ".join(scope_parts) + ")" if scope_parts else ""
job_lines: list[str] = []
for index, job in enumerate(job_list, start=1):
title = str(job.get("title", "Untitled"))
company = str(job.get("company", "Unknown company"))
location = str(job.get("location", "N/A"))
url = str(job.get("url", ""))
line = f"{index}. {title}{company} ({location})"
job_lines.append(line)
if url:
job_lines.append(f" {url}")
if job_lines:
jobs_section = "\n" + "\n".join(job_lines)
else:
jobs_section = "\nNo jobs matched this alert."
jobs_message = jobs_section.strip()
context: Dict[str, Any] = {
"count": len(job_list),
"count_label": "No new jobs" if not job_list else f"{len(job_list)} new jobs",
"scope": scope,
"region": region or "",
"keyword": keyword or "",
"timestamp": timestamp,
"generated_at": generated_at,
"intro_line": "Here are the latest jobs discovered by the scraper:",
"jobs_message": jobs_message,
"jobs_section": jobs_section,
"jobs_lines": job_lines,
"has_jobs": bool(job_list),
}
template = template_override
if template is None:
try:
from web.db import get_email_template_by_slug
template = get_email_template_by_slug("job-alert")
except Exception:
template = None
template_subject = (template or {}).get(
"subject") or DEFAULT_JOB_ALERT_SUBJECT
template_body = (template or {}).get("body") or DEFAULT_JOB_ALERT_BODY
subject = _format_template(template_subject, context)
body = _format_template(template_body, context)
result = {
"subject": subject,
"body": body,
"context": context,
"template_slug": (template or {}).get("slug", "job-alert"),
}
return result