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= → returns Notification • Bulk — users= → 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