feat(newsletter): Add subscription confirmation email functionality
All checks were successful
CI / test (3.11) (push) Successful in 9m26s
CI / build-image (push) Successful in 49s

- Implemented `send_subscription_confirmation` function to send a confirmation email upon subscription.
- Added a default HTML template for the confirmation email in settings.
- Updated the newsletter management page to handle subscription and unsubscription actions.
- Created new templates for embedding contact and newsletter forms.
- Added styles for the new templates and unified CSS styles across the application.
- Updated tests to reflect changes in the newsletter management API endpoints.
This commit is contained in:
2025-10-30 12:38:26 +01:00
parent f7695be8ef
commit 56840ac313
24 changed files with 897 additions and 449 deletions

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from flask import Flask
from . import admin, auth, contact, monitoring, newsletter
from . import admin, auth, contact, embed, monitoring, newsletter
def register_blueprints(app: Flask) -> None:
@@ -13,3 +13,4 @@ def register_blueprints(app: Flask) -> None:
app.register_blueprint(monitoring.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(admin.bp)
app.register_blueprint(embed.bp)

View File

@@ -82,6 +82,13 @@ def submissions():
return render_template("admin_submissions.html")
@bp.route("/embeds")
@auth.login_required
def embeds():
"""Display embeddable forms management page."""
return render_template("admin_embeds.html")
@bp.route("/api/settings", methods=["GET"])
@auth.login_required
def get_settings_api():

26
server/routes/embed.py Normal file
View File

@@ -0,0 +1,26 @@
"""Embeddable forms routes."""
from __future__ import annotations
from flask import Blueprint, render_template, send_from_directory
import os
bp = Blueprint("embed", __name__)
@bp.route("/embed/contact", methods=["GET"])
def contact_form():
"""Serve the embeddable contact form."""
return render_template("embed_contact.html")
@bp.route("/embed/newsletter", methods=["GET"])
def newsletter_form():
"""Serve the embeddable newsletter subscription form."""
return render_template("embed_newsletter.html")
@bp.route("/static/css/styles.css", methods=["GET"])
def serve_css():
"""Serve the unified CSS file."""
static_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'static')
return send_from_directory(static_dir, 'css/styles.css')

View File

@@ -7,10 +7,10 @@ from flask import Blueprint, jsonify, request, render_template
from ..services import newsletter
bp = Blueprint("newsletter", __name__, url_prefix="/api")
bp = Blueprint("newsletter", __name__)
@bp.route("/newsletter", methods=["POST"])
@bp.route("/api/newsletter", methods=["POST"])
def subscribe():
payload = request.form or request.get_json(silent=True) or {}
email = (payload.get("email") or "").strip()
@@ -29,10 +29,16 @@ def subscribe():
return jsonify({"status": "error", "message": "Email is already subscribed."}), 409
logging.info("New newsletter subscription: %s", email)
# Send confirmation email
email_sent = newsletter.send_subscription_confirmation(email)
if not email_sent:
logging.warning("Confirmation email not sent for %s", email)
return jsonify({"status": "ok", "message": "Subscribed successfully."}), 201
@bp.route("/newsletter", methods=["DELETE"])
@bp.route("/api/newsletter", methods=["DELETE"])
def unsubscribe():
payload = request.form or request.get_json(silent=True) or {}
email = (payload.get("email") or "").strip()
@@ -55,7 +61,7 @@ def unsubscribe():
return jsonify({"status": "ok", "message": "Unsubscribed successfully."}), 200
@bp.route("/newsletter", methods=["PUT"])
@bp.route("/api/newsletter", methods=["PUT"])
def update_subscription():
payload = request.form or request.get_json(silent=True) or {}
old_email = (payload.get("old_email") or "").strip()
@@ -104,8 +110,7 @@ def manage_subscription():
elif action == "unsubscribe":
deleted = newsletter.unsubscribe(email)
if deleted:
message = "Successfully unsubscribed from newsletter."
message_type = "success"
return render_template("unsubscribe_confirmation.html")
else:
message = "This email is not currently subscribed."
message_type = "info"
@@ -131,3 +136,9 @@ def manage_subscription():
message_type = "error"
return render_template("newsletter_manage.html", message=message, message_type=message_type)
@bp.route("/newsletter/unsubscribe/confirmation", methods=["GET"])
def unsubscribe_confirmation():
"""Display newsletter unsubscription confirmation page."""
return render_template("unsubscribe_confirmation.html")