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:
145
backend/notifications/models.py
Normal file
145
backend/notifications/models.py
Normal 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
|
||||
Reference in New Issue
Block a user