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:
62
web/templates/admin/email.html
Normal file
62
web/templates/admin/email.html
Normal file
@@ -0,0 +1,62 @@
|
||||
{% extends 'base.html' %} {% block content %}
|
||||
<h2>Email Subscriptions</h2>
|
||||
<section>
|
||||
<h3>Add Subscription</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="subscribe" />
|
||||
<label for="email">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="alerts@example.com"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Current Recipients</h3>
|
||||
{% if not subscriptions %}
|
||||
<p>No subscriptions yet. Add one above to start sending alerts.</p>
|
||||
<p>You can customize alert content from the <a href="{{ url_for('admin_email_templates') }}">Email Templates</a> page.</p>
|
||||
{% else %}
|
||||
<p>{{ total_active }} active of {{ total }} total.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for sub in subscriptions %}
|
||||
<tr>
|
||||
<td>{{ sub.email }}</td>
|
||||
<td>{{ 'Active' if sub.is_active else 'Inactive' }}</td>
|
||||
<td>{{ sub.created_at }}</td>
|
||||
<td>{{ sub.updated_at }}</td>
|
||||
<td>
|
||||
<form method="post" style="display: inline-flex; gap: 0.5rem">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="email" value="{{ sub.email }}" />
|
||||
{% if sub.is_active %}
|
||||
<input type="hidden" name="action" value="unsubscribe" />
|
||||
<button type="submit">Deactivate</button>
|
||||
{% else %}
|
||||
<input type="hidden" name="action" value="reactivate" />
|
||||
<button type="submit">Reactivate</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
102
web/templates/admin/email_templates.html
Normal file
102
web/templates/admin/email_templates.html
Normal file
@@ -0,0 +1,102 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<h2>Email Templates</h2>
|
||||
<section>
|
||||
<h3>Available Templates</h3>
|
||||
{% if not templates %}
|
||||
<p>No templates found. Create one below to get started.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Status</th>
|
||||
<th>Updated</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for template in templates %}
|
||||
<tr>
|
||||
<td>{{ template.name }}</td>
|
||||
<td>{{ template.slug }}</td>
|
||||
<td>{{ 'Active' if template.is_active else 'Inactive' }}</td>
|
||||
<td>{{ template.updated_at or template.created_at or '' }}</td>
|
||||
<td style="display: flex; gap: 0.5rem;">
|
||||
<a class="button" href="{{ url_for('admin_email_templates', template_id=template.template_id) }}">Edit</a>
|
||||
<a class="button" href="{{ url_for('admin_email_templates', preview_id=template.template_id) }}">Preview</a>
|
||||
<form method="post" onsubmit="return confirm('Delete template {{ template.name }}?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="delete" />
|
||||
<input type="hidden" name="template_id" value="{{ template.template_id }}" />
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
<section>
|
||||
<h3>{{ 'Edit Template' if editing else 'Create Template' }}</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="{{ 'update' if editing else 'create' }}" />
|
||||
{% if editing %}
|
||||
<input type="hidden" name="template_id" value="{{ editing.template_id }}" />
|
||||
{% endif %}
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" value="{{ editing.name if editing else '' }}" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="slug">Slug</label>
|
||||
<input type="text" id="slug" name="slug" placeholder="job-alert" value="{{ editing.slug if editing else '' }}" />
|
||||
<small>Leave blank to reuse the name. Slug must be URL friendly (letters, numbers, dashes).</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject">Subject Template</label>
|
||||
<input type="text" id="subject" name="subject" value="{{ editing.subject if editing else '' }}" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="body">Body Template</label>
|
||||
<textarea id="body" name="body" rows="12" required>{{ editing.body if editing else '' }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" {% if editing is none or editing.is_active %}checked{% endif %} />
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit">{{ 'Update Template' if editing else 'Create Template' }}</button>
|
||||
{% if editing %}
|
||||
<a class="button" href="{{ url_for('admin_email_templates') }}">Cancel</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
<aside>
|
||||
<h4>Available placeholders</h4>
|
||||
<ul>
|
||||
<li><code>{count}</code> – number of jobs in the alert</li>
|
||||
<li><code>{count_label}</code> – "No new jobs" or "X new jobs"</li>
|
||||
<li><code>{scope}</code> – formatted region/keyword context</li>
|
||||
<li><code>{region}</code>, <code>{keyword}</code></li>
|
||||
<li><code>{timestamp}</code> – formatted timestamp</li>
|
||||
<li><code>{jobs_section}</code> – newline-prefixed block of job entries</li>
|
||||
<li><code>{jobs_message}</code> – jobs block without leading newline</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</section>
|
||||
{% if preview %}
|
||||
<section>
|
||||
<h3>Preview: {{ preview_template.name if preview_template else 'Job Alert' }}</h3>
|
||||
<article>
|
||||
<h4>Subject</h4>
|
||||
<pre>{{ preview.subject }}</pre>
|
||||
<h4>Body</h4>
|
||||
<pre>{{ preview.body }}</pre>
|
||||
</article>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -16,17 +16,21 @@
|
||||
<header>
|
||||
<h1><a href="/">{{ title or 'Admin' }}</a></h1>
|
||||
<nav>
|
||||
{% if username %}<span>Hi, {{ username }}</span> | {% endif %}
|
||||
<a href="{{ url_for('index') }}">Home</a> |
|
||||
<a href="{{ url_for('user_settings') }}">Preferences</a>
|
||||
{% if current_user and current_user.is_admin %} |
|
||||
<a href="{{ url_for('scrape_page') }}">Scrape Jobs</a> |
|
||||
<a href="{{ url_for('admin_taxonomy') }}">Taxonomy</a> |
|
||||
<a href="{{ url_for('admin_stats') }}">Statistics</a> |
|
||||
<a href="{{ url_for('admin_users') }}">Users</a> {% endif %} {% if
|
||||
session.get('username') %} |
|
||||
<a href="{{ url_for('logout') }}">Logout</a> {% else %} |
|
||||
<a href="{{ url_for('login') }}">Login</a>{% endif %}
|
||||
<div id="navigation">
|
||||
{% if username %}<span>Hi, {{ username }}</span> | {% endif %}
|
||||
<a href="{{ url_for('index') }}">Home</a> |
|
||||
<a href="{{ url_for('user_settings') }}">Preferences</a>
|
||||
{% if current_user and current_user.is_admin %} |
|
||||
<a href="{{ url_for('scrape_page') }}">Scrape Jobs</a> |
|
||||
<a href="{{ url_for('admin_taxonomy') }}">Taxonomy</a> |
|
||||
<a href="{{ url_for('admin_stats') }}">Statistics</a> |
|
||||
<a href="{{ url_for('admin_emails') }}">Email Alerts</a> |
|
||||
<a href="{{ url_for('admin_email_templates') }}">Email Templates</a> |
|
||||
<a href="{{ url_for('admin_users') }}">Users</a> {% endif %} {% if
|
||||
session.get('username') %} |
|
||||
<a href="{{ url_for('logout') }}">Logout</a> {% else %} |
|
||||
<a href="{{ url_for('login') }}">Login</a>{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% with messages = get_flashed_messages() %} {% if messages %}
|
||||
<ul>
|
||||
|
||||
@@ -77,6 +77,29 @@ block content %}
|
||||
<p>No keywords available. Ask an admin to add some.</p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Negative Keywords</legend>
|
||||
<p>
|
||||
<small>Add new Negative Keyword:</small>
|
||||
<input
|
||||
type="text"
|
||||
name="new-negative-keyword"
|
||||
id="new-negative-keyword"
|
||||
value=""
|
||||
placeholder="Type a keyword and save to add"
|
||||
size="30"
|
||||
/>
|
||||
</p>
|
||||
{% if user_negative_keywords %} {% for nk in user_negative_keywords %}
|
||||
<label style="display: block">
|
||||
<input type="checkbox" name="negative_keyword" value="{{ nk }}" checked />
|
||||
{{ nk }}
|
||||
</label>
|
||||
{% endfor %} {% else %}
|
||||
<p>No negative keywords set.</p>
|
||||
{% endif %}
|
||||
<p><small>Uncheck to remove.</small></p>
|
||||
</fieldset>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
{% endblock %} {% block footer_scripts %}
|
||||
|
||||
Reference in New Issue
Block a user