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.
146 lines
5.4 KiB
Python
146 lines
5.4 KiB
Python
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
|