feat: Implement email sending utilities and templates for job notifications
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:
2025-11-28 18:15:08 +01:00
parent 8afb208985
commit 2185a07ff0
23 changed files with 2660 additions and 63 deletions

View File

@@ -125,6 +125,66 @@ def get_base_url() -> str:
return get_config().get('scraper', {}).get('base_url', "https://{region}.craigslist.org/search/jjj?query={keyword}&sort=rel")
def get_negative_keywords() -> List[str]:
"""Return normalized list of negative keywords from config."""
raw = get_config().get('scraper', {}).get('negative_keywords', [])
if not isinstance(raw, list):
return []
cleaned: List[str] = []
for item in raw:
if not isinstance(item, str):
continue
val = item.strip()
if not val:
continue
cleaned.append(val.lower())
return cleaned
def get_email_settings() -> Dict[str, Any]:
"""Return normalized email settings from config."""
cfg = get_config().get('email', {})
if not isinstance(cfg, dict):
cfg = {}
raw_smtp = cfg.get('smtp', {}) if isinstance(cfg.get('smtp'), dict) else {}
raw_recipients = cfg.get('recipients', [])
def _to_int(value, default):
try:
return int(value)
except (TypeError, ValueError):
return default
recipients: List[str] = []
if isinstance(raw_recipients, list):
for item in raw_recipients:
if isinstance(item, str):
addr = item.strip()
if addr:
recipients.append(addr)
smtp = {
'host': (raw_smtp.get('host') or '').strip(),
'port': _to_int(raw_smtp.get('port', 587), 587),
'username': (raw_smtp.get('username') or '').strip(),
'password': raw_smtp.get('password') or '',
'use_tls': bool(raw_smtp.get('use_tls', True)),
'use_ssl': bool(raw_smtp.get('use_ssl', False)),
'timeout': _to_int(raw_smtp.get('timeout', 30), 30),
}
if smtp['port'] <= 0:
smtp['port'] = 587
if smtp['timeout'] <= 0:
smtp['timeout'] = 30
return {
'enabled': bool(cfg.get('enabled', False)),
'from_address': (cfg.get('from_address') or '').strip(),
'smtp': smtp,
'recipients': recipients,
}
def now_iso() -> str:
"""Get the current time in ISO format."""
return datetime.now(UTC).isoformat()
@@ -203,13 +263,39 @@ def filter_jobs(
jobs: List[Dict[str, Any]],
region: Optional[str] = None,
keyword: Optional[str] = None,
negative_keywords: Optional[List[str]] = None,
) -> List[Dict[str, Any]]:
"""Filter jobs by optional region and keyword."""
"""Filter jobs by optional region, keyword, and negative keywords."""
filtered = jobs
if region:
filtered = [j for j in filtered if j.get("region") == region]
if keyword:
filtered = [j for j in filtered if j.get("keyword") == keyword]
if negative_keywords:
# Pre-compile regexes or just check substring?
# Scraper uses substring check. Let's do the same for consistency.
# Fields to check: title, company, location, description
# Note: description might contain HTML or be long.
# Normalize negative keywords
nks = [nk.lower() for nk in negative_keywords if nk]
def is_clean(job):
# Check all fields
text_blob = " ".join([
str(job.get("title") or ""),
str(job.get("company") or ""),
str(job.get("location") or ""),
str(job.get("description") or "")
]).lower()
for nk in nks:
if nk in text_blob:
return False
return True
filtered = [j for j in filtered if is_clean(j)]
return filtered