Introduce notifications app and integrate emails

Add a new notifications app (models, serializers, views, admin, tasks, consumers, routing, urls, tests) with Notification.notify for in-app websocket pushes and optional email delivery. Centralize email sending in notifications.tasks.send_email_with_context and re-export it from account.tasks for compatibility. Update account and commerce and advertisement tasks to use Notification.notify/_notify_order and the new email helper; adjust order/notification tasks to consolidate logic. Wire notifications into ASGI routing, settings and URL conf. Misc: handle OSError when importing weasyprint, add tmp/ to .gitignore, add local .claude PowerShell checks, add social.blog skeleton, and remove legacy ews-component test files.
This commit is contained in:
David Bruno Vontor
2026-06-09 16:18:41 +02:00
parent 46bc131a56
commit 2592a69790
74 changed files with 666 additions and 13194 deletions

View File

@@ -0,0 +1,119 @@
from datetime import datetime
from celery import shared_task
from celery.utils.log import get_task_logger
from django.core.mail import send_mail, EmailMultiAlternatives
logger = get_task_logger(__name__)
from account.models import CustomUser
from django.conf import settings
from django.template.loader import render_to_string
@shared_task
def send_notification_email_task(recipient_email, subject, template_path, context=None, attachments=None):
send_email_with_context(
recipients=recipient_email,
subject=subject,
template_path=template_path,
context=context or {},
attachments=attachments,
)
def send_email_with_context(recipients, subject, template_path=None, context=None, message: str | None = None, attachments=None):
"""
Send email using component-based template system.
Uses base.html with header/footer components and includes the specified content template.
"""
if isinstance(recipients, str):
recipients = [recipients]
if not recipients:
logger.warning("No recipients provided for email. Skipping sending email.")
return "No recipients provided for email."
ctx = dict(context or {})
# Add current_year to context if not present
if "current_year" not in ctx:
ctx["current_year"] = datetime.now().year
# Add AppConfig data for footer if not already present
if "company_name" not in ctx:
try:
from configuration.models import AppConfig
config = AppConfig.objects.first()
if config:
ctx["company_name"] = config.company_name
ctx["company_address"] = config.company_address
ctx["company_ico"] = config.company_ico
ctx["company_dic"] = config.company_dic
ctx["contact_email"] = config.contact_email
ctx["contact_phone"] = config.contact_phone
except Exception:
pass # Gracefully skip if AppConfig not available
# Sanitize user if someone passes the model by mistake
if "user" in ctx and not isinstance(ctx["user"], dict):
try:
ctx["user"] = _build_user_template_ctx(ctx["user"])
except Exception:
ctx["user"] = {}
# Render HTML using base template with content include
html_message = None
if template_path:
ctx["content_template"] = template_path
html_message = render_to_string("email/components/base.html", ctx)
try:
email = EmailMultiAlternatives(
subject=subject,
body=message or "Tento e-mail vyžaduje HTML podporu.",
from_email=None,
to=recipients if len(recipients) == 1 else [],
bcc=recipients if len(recipients) > 1 else [],
)
if html_message:
email.attach_alternative(html_message, "text/html")
if attachments:
for filename, content, mimetype in attachments:
email.attach(filename, content, mimetype)
email.send(fail_silently=False)
logger.info(f"Sent email to {recipients} with subject '{subject}'")
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug(f"\nEMAIL HTML:\n{html_message}\nKONEC OBSAHU")
return "Successfully sent email."
except Exception as e:
logger.error(f"E-mail se neodeslal: {e}")
raise # Re-raise so Celery marks the task as FAILED and can retry
def _build_user_template_ctx(user: CustomUser) -> dict:
"""
Return a plain dict for templates instead of passing the DB model.
Provides aliases to avoid template errors (firstname vs first_name).
Adds a backward-compatible key 'get_full_name' for templates using user.get_full_name.
"""
first_name = getattr(user, "first_name", "") or ""
last_name = getattr(user, "last_name", "") or ""
full_name = f"{first_name} {last_name}".strip()
return {
"id": user.pk,
"email": getattr(user, "email", "") or "",
"first_name": first_name,
"firstname": first_name, # alias for templates using `firstname`
"last_name": last_name,
"lastname": last_name, # alias for templates using `lastname`
"full_name": full_name,
"get_full_name": full_name, # compatibility for templates using method-style access
}