feat: Implement email sending utilities and templates for job notifications
Some checks failed
CI/CD Pipeline / test (push) Failing after 4m9s
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.
This commit is contained in:
106
web/email_templates.py
Normal file
106
web/email_templates.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user