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,145 @@
import logging
from django.db import models
from account.models import CustomUser
from vontor_cz.models import SoftDeleteModel
logger = logging.getLogger(__name__)
class Notification(SoftDeleteModel):
class Type(models.TextChoices):
SYSTEM = "system", "Systém"
ORDER = "order", "Objednávka"
PAYMENT = "payment", "Platba"
SOCIAL = "social", "Sociální"
CHAT = "chat", "Chat"
ADVERTISEMENT = "advertisement", "Inzerát"
title = models.CharField(max_length=200, help_text="Předmět oznámení.")
text = models.TextField(help_text="Obsah oznámení.")
notification_type = models.CharField(
max_length=20,
choices=Type.choices,
default=Type.SYSTEM,
help_text="Kategorie oznámení — používá se pro ikonky a filtrování na frontendu.",
)
action_url = models.CharField(
max_length=500,
null=True,
blank=True,
help_text="Volitelný odkaz na detail (např. '/objednavky/123/').",
)
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(
CustomUser, on_delete=models.CASCADE, null=True, related_name='notifications',
help_text="Příjemce oznámení.",
)
bulk = models.BooleanField(
default=False,
help_text="True, pokud bylo oznámení vytvořeno hromadně pro více uživatelů.",
)
is_read = models.BooleanField(default=False)
read_at = models.DateTimeField(null=True, blank=True)
send_email = models.BooleanField(
default=False,
help_text="True, pokud byl zároveň odeslán e-mail.",
)
email_subject = models.CharField(max_length=255, null=True, blank=True)
email_template_path = models.CharField(max_length=255, null=True, blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
@classmethod
def notify(cls, user=None, title=None, text=None, *, users=None,
notification_type=None, action_url=None,
email_subject=None, template_path=None,
email_context=None, attachments=None, bulk=False):
"""Create an in-app notification and optionally send an email + WebSocket push.
Modes:
• Single — user=<CustomUser> → returns Notification
• Bulk — users=<list|qs> → returns list[Notification]
notification_type controls the frontend icon/colour (default: system).
action_url is an optional relative path the bell badge links to.
If template_path is set, an email is dispatched (Celery if CELERY_ENABLED, else sync).
"""
if users is not None:
return [
cls.notify(
user=u, title=title, text=text,
notification_type=notification_type, action_url=action_url,
email_subject=email_subject, template_path=template_path,
email_context=email_context, attachments=attachments,
bulk=True,
)
for u in users
]
if user is None:
raise ValueError("Notification.notify() requires either 'user' or 'users'.")
notification = cls.objects.create(
user=user,
title=title,
text=text,
notification_type=notification_type or cls.Type.SYSTEM,
action_url=action_url,
bulk=bulk,
send_email=bool(template_path),
email_subject=email_subject,
email_template_path=template_path,
)
# Real-time push via WebSocket
try:
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
channel_layer = get_channel_layer()
if channel_layer:
async_to_sync(channel_layer.group_send)(
f"notifications_{user.pk}",
{
"type": "notification.new",
"id": notification.pk,
"title": notification.title,
"text": notification.text,
"notification_type": notification.notification_type,
"action_url": notification.action_url,
"created_at": notification.created_at.isoformat(),
},
)
except Exception as exc:
logger.warning("[Notification.notify] WebSocket push failed for user %s: %s", user.pk, exc)
if template_path and getattr(user, 'email', None):
try:
from django.conf import settings
from notifications.tasks import send_notification_email_task
ctx = dict(email_context or {})
if 'user' not in ctx:
ctx['user'] = user
kwargs = dict(
recipient_email=user.email,
subject=email_subject or title,
template_path=template_path,
context=ctx,
attachments=attachments,
)
if getattr(settings, 'CELERY_ENABLED', False):
send_notification_email_task.delay(**kwargs)
else:
send_notification_email_task(**kwargs)
except Exception as exc:
logger.warning("[Notification.notify] Email failed for user %s: %s", user.pk, exc)
return notification