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:
88
web/utils.py
88
web/utils.py
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user