Some checks failed
CI/CD Pipeline / test (push) Failing after 4m9s
- 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.
107 lines
3.4 KiB
Python
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
|