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:
@@ -1,121 +1,80 @@
|
||||
from celery import shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.template.loader import render_to_string
|
||||
from .tokens import *
|
||||
from .models import CustomUser
|
||||
|
||||
# Re-exported so existing imports from account.tasks still work
|
||||
from notifications.tasks import send_email_with_context # noqa: F401
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
def send_email_with_context(recipients, subject, template_path=None, context=None, message: str | None = None):
|
||||
"""
|
||||
Send emails rendering a single HTML template.
|
||||
- `template_name` is a simple base name without extension, e.g. "email/test".
|
||||
- Renders only HTML (".html"), no ".txt" support.
|
||||
- Converts `user` in context to a plain dict to avoid passing models to templates.
|
||||
"""
|
||||
if isinstance(recipients, str):
|
||||
recipients = [recipients]
|
||||
|
||||
html_message = None
|
||||
if template_path:
|
||||
ctx = dict(context or {})
|
||||
# Render base layout and include the provided template as the main content.
|
||||
# The included template receives the same context as the base.
|
||||
html_message = render_to_string(
|
||||
"email/components/base.html",
|
||||
{"content_template": template_path, **ctx},
|
||||
)
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=message or "",
|
||||
from_email=None,
|
||||
recipient_list=recipients,
|
||||
fail_silently=False,
|
||||
html_message=html_message,
|
||||
)
|
||||
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend' and message:
|
||||
logger.debug(f"\nEMAIL OBSAH:\n{message}\nKONEC OBSAHU")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"E-mail se neodeslal: {e}")
|
||||
return False
|
||||
|
||||
|
||||
#----------------------------------------------------------------------------------------------------
|
||||
|
||||
# This function sends an email to the user for email verification after registration.
|
||||
@shared_task
|
||||
def send_email_verification_task(user_id):
|
||||
from notifications.models import Notification
|
||||
try:
|
||||
user = CustomUser.objects.get(pk=user_id)
|
||||
except CustomUser.DoesNotExist:
|
||||
logger.info(f"Task send_email_verification has failed. Invalid User ID was sent.")
|
||||
return 0
|
||||
|
||||
logger.info("send_email_verification_task: invalid user_id %s", user_id)
|
||||
return
|
||||
|
||||
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||
# {changed} generate and store a per-user token
|
||||
token = user.generate_email_verification_token()
|
||||
verify_url = f"{settings.FRONTEND_URL}/email-verification/?uidb64={uid}&token={token}"
|
||||
|
||||
context = {
|
||||
"user": user,
|
||||
"action_url": verify_url,
|
||||
"frontend_url": settings.FRONTEND_URL,
|
||||
"cta_label": "Ověřit e‑mail",
|
||||
}
|
||||
|
||||
send_email_with_context(
|
||||
recipients=user.email,
|
||||
subject="Ověření e‑mailu",
|
||||
Notification.notify(
|
||||
user=user,
|
||||
title="Ověření e‑mailu",
|
||||
text="Prosím ověřte svou e-mailovou adresu kliknutím na odkaz v e-mailu.",
|
||||
action_url=verify_url,
|
||||
template_path="email/email_verification.html",
|
||||
context=context,
|
||||
email_context={
|
||||
"action_url": verify_url,
|
||||
"frontend_url": settings.FRONTEND_URL,
|
||||
"cta_label": "Ověřit e‑mail",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_email_test_task(email):
|
||||
context = {
|
||||
"action_url": settings.FRONTEND_URL,
|
||||
"frontend_url": settings.FRONTEND_URL,
|
||||
"cta_label": "Otevřít aplikaci",
|
||||
}
|
||||
send_email_with_context(
|
||||
recipients=email,
|
||||
subject="Testovací e‑mail",
|
||||
template_path="email/test.html",
|
||||
context=context,
|
||||
context={
|
||||
"action_url": settings.FRONTEND_URL,
|
||||
"frontend_url": settings.FRONTEND_URL,
|
||||
"cta_label": "Otevřít aplikaci",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_password_reset_email_task(user_id):
|
||||
from notifications.models import Notification
|
||||
try:
|
||||
user = CustomUser.objects.get(pk=user_id)
|
||||
except CustomUser.DoesNotExist:
|
||||
logger.info(f"Task send_password_reset_email has failed. Invalid User ID was sent.")
|
||||
return 0
|
||||
|
||||
logger.info("send_password_reset_email_task: invalid user_id %s", user_id)
|
||||
return
|
||||
|
||||
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||
token = password_reset_token.make_token(user)
|
||||
reset_url = f"{settings.FRONTEND_URL}/reset-password/{uid}/{token}"
|
||||
|
||||
context = {
|
||||
"user": user,
|
||||
"action_url": reset_url,
|
||||
"frontend_url": settings.FRONTEND_URL,
|
||||
"cta_label": "Obnovit heslo",
|
||||
}
|
||||
|
||||
send_email_with_context(
|
||||
recipients=user.email,
|
||||
subject="Obnova hesla",
|
||||
Notification.notify(
|
||||
user=user,
|
||||
title="Obnova hesla",
|
||||
text="Byl vyžádán reset vašeho hesla. Klikněte na odkaz pro nastavení nového hesla.",
|
||||
action_url=reset_url,
|
||||
template_path="email/password_reset.html",
|
||||
context=context,
|
||||
)
|
||||
email_context={
|
||||
"action_url": reset_url,
|
||||
"frontend_url": settings.FRONTEND_URL,
|
||||
"cta_label": "Obnovit heslo",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -237,7 +237,9 @@ class UserView(viewsets.ModelViewSet):
|
||||
|
||||
def get_serializer_class(self):
|
||||
user = getattr(self.request, 'user', None)
|
||||
|
||||
is_admin = user and (getattr(user, 'role', None) == 'admin' or getattr(user, 'is_superuser', False))
|
||||
|
||||
if self.action in ['retrieve', 'list'] and not is_admin:
|
||||
return PublicUserSerializer
|
||||
return CustomUserSerializer
|
||||
@@ -277,11 +279,15 @@ class ChangePasswordView(APIView):
|
||||
def post(self, request):
|
||||
serializer = ChangePasswordSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
user = request.user
|
||||
|
||||
if not user.check_password(serializer.validated_data['current_password']):
|
||||
return Response({"current_password": "Nesprávné heslo."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user.set_password(serializer.validated_data['new_password'])
|
||||
user.save()
|
||||
|
||||
return Response({"detail": "Heslo bylo úspěšně změněno."})
|
||||
|
||||
|
||||
|
||||
@@ -1,47 +1,35 @@
|
||||
from venv import create
|
||||
from account.tasks import send_email_with_context
|
||||
from notifications.tasks import send_email_with_context
|
||||
from configuration.models import SiteConfiguration
|
||||
|
||||
from celery import shared_task
|
||||
from celery.schedules import crontab
|
||||
|
||||
from commerce.models import Product
|
||||
import datetime
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_contact_me_email_task(client_email, message_content):
|
||||
context = {
|
||||
"client_email": client_email,
|
||||
"message_content": message_content
|
||||
}
|
||||
config_email = SiteConfiguration.get_solo().contact_email
|
||||
recipient = config_email if config_email else "brunovontor@gmail.com"
|
||||
send_email_with_context(
|
||||
recipients=recipient,
|
||||
subject="Poptávka z kontaktního formuláře!!!",
|
||||
template_path="email/contact_me.html",
|
||||
context=context,
|
||||
context={
|
||||
"client_email": client_email,
|
||||
"message_content": message_content,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_newly_added_items_to_store_email_task_last_week():
|
||||
last_week_date = datetime.datetime.now() - datetime.timedelta(days=7)
|
||||
|
||||
"""
|
||||
__lte -> Less than or equal
|
||||
__gte -> Greater than or equal
|
||||
__lt -> Less than
|
||||
__gt -> Greater than
|
||||
"""
|
||||
|
||||
products_of_week = Product.objects.filter(
|
||||
include_in_week_summary_email=True,
|
||||
created_at__gte=last_week_date
|
||||
created_at__gte=last_week_date,
|
||||
)
|
||||
|
||||
config = SiteConfiguration.get_solo()
|
||||
|
||||
send_email_with_context(
|
||||
recipients=config.contact_email,
|
||||
subject="Nový produkt přidán do obchodu",
|
||||
@@ -49,6 +37,5 @@ def send_newly_added_items_to_store_email_task_last_week():
|
||||
context={
|
||||
"products_of_week": products_of_week,
|
||||
"site_currency": config.currency,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator, validat
|
||||
|
||||
try:
|
||||
from weasyprint import HTML
|
||||
except ImportError:
|
||||
except (ImportError, OSError):
|
||||
HTML = None
|
||||
|
||||
import os
|
||||
|
||||
@@ -1,182 +1,157 @@
|
||||
from account.tasks import send_email_with_context
|
||||
from celery import shared_task
|
||||
|
||||
from django.apps import apps
|
||||
from django.utils import timezone
|
||||
|
||||
from notifications.models import Notification
|
||||
from notifications.tasks import send_email_with_context
|
||||
|
||||
|
||||
def _notify_order(order, title, text, template_path, action_url=None):
|
||||
"""Send in-app notification + email for logged-in users, email-only for guests."""
|
||||
if order.user:
|
||||
Notification.notify(
|
||||
user=order.user,
|
||||
title=title,
|
||||
text=text,
|
||||
notification_type=Notification.Type.ORDER,
|
||||
action_url=action_url or f"/objednavky/{order.id}/",
|
||||
template_path=template_path,
|
||||
email_context={"order": order},
|
||||
)
|
||||
else:
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject=title,
|
||||
template_path=template_path,
|
||||
context={"order": order},
|
||||
)
|
||||
|
||||
|
||||
# -- CLEANUP TASKS --
|
||||
|
||||
# Delete expired/cancelled orders (older than 24 hours)
|
||||
@shared_task
|
||||
def delete_expired_orders():
|
||||
Order = apps.get_model('commerce', 'Order')
|
||||
|
||||
expired_orders = Order.objects.filter(status=Order.OrderStatus.CANCELLED, created_at__lt=timezone.now() - timezone.timedelta(hours=24))
|
||||
expired_orders = Order.objects.filter(
|
||||
status=Order.OrderStatus.CANCELLED,
|
||||
created_at__lt=timezone.now() - timezone.timedelta(hours=24),
|
||||
)
|
||||
count = expired_orders.count()
|
||||
expired_orders.delete()
|
||||
return count
|
||||
|
||||
|
||||
# -- NOTIFICATIONS CARRIER --
|
||||
# -- CARRIER NOTIFICATIONS --
|
||||
|
||||
# Zásilkovna
|
||||
@shared_task
|
||||
def notify_zasilkovna_sended(order = None, **kwargs):
|
||||
def notify_zasilkovna_sended(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_zasilkovna_sended:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your order has been shipped",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Vaše objednávka byla odeslána",
|
||||
text=f"Objednávka #{order.id} byla předána přepravci Zásilkovna.",
|
||||
template_path="email/shipping/zasilkovna/zasilkovna_sended.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
# Shop
|
||||
@shared_task
|
||||
def notify_Ready_to_pickup(order = None, **kwargs):
|
||||
def notify_Ready_to_pickup(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_Ready_to_pickup:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your order is ready for pickup",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Vaše objednávka je připravena k vyzvednutí",
|
||||
text=f"Objednávka #{order.id} čeká na vyzvednutí na prodejně.",
|
||||
template_path="email/shipping/ready_to_pickup/ready_to_pickup.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
# -- NOTIFICATIONS ORDER --
|
||||
# -- ORDER NOTIFICATIONS --
|
||||
|
||||
@shared_task
|
||||
def notify_order_successfuly_created(order = None, **kwargs):
|
||||
def notify_order_successfuly_created(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_order_successfuly_created:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your order has been successfully created",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Objednávka byla úspěšně vytvořena",
|
||||
text=f"Vaše objednávka #{order.id} byla přijata a čeká na zpracování.",
|
||||
template_path="email/order_created.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def notify_order_payed(order = None, **kwargs):
|
||||
def notify_order_payed(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_order_payed:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your order has been paid",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Platba objednávky přijata",
|
||||
text=f"Platba za objednávku #{order.id} byla úspěšně přijata.",
|
||||
template_path="email/order_paid.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def notify_about_missing_payment(order = None, **kwargs):
|
||||
def notify_about_missing_payment(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_about_missing_payment:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Payment missing for your order",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Nezaplacená objednávka",
|
||||
text=f"Objednávka #{order.id} dosud nebyla zaplacena. Dokončete platbu co nejdříve.",
|
||||
template_path="email/order_missing_payment.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
# -- NOTIFICATIONS REFUND --
|
||||
# -- REFUND NOTIFICATIONS --
|
||||
|
||||
@shared_task
|
||||
def notify_refund_items_arrived(order = None, **kwargs):
|
||||
def notify_refund_items_arrived(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_refund_items_arrived:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your refund items have arrived",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Vrácené zboží přijato",
|
||||
text=f"Vrácené zboží k objednávce #{order.id} bylo přijato. Reklamace bude zpracována.",
|
||||
template_path="email/order_refund_items_arrived.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
# Refund accepted, returning money
|
||||
@shared_task
|
||||
def notify_refund_accepted(order = None, **kwargs):
|
||||
def notify_refund_accepted(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_refund_accepted:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your refund has been accepted",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Vrácení peněz schváleno",
|
||||
text=f"Vaše reklamace k objednávce #{order.id} byla schválena. Peníze budou vráceny.",
|
||||
template_path="email/order_refund_accepted.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
# -- NOTIFICATIONS ORDER STATUS --
|
||||
# -- ORDER STATUS NOTIFICATIONS --
|
||||
|
||||
@shared_task
|
||||
def notify_order_cancelled(order = None, **kwargs):
|
||||
def notify_order_cancelled(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_order_cancelled:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your order has been cancelled",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Objednávka zrušena",
|
||||
text=f"Objednávka #{order.id} byla zrušena.",
|
||||
template_path="email/order_cancelled.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@shared_task
|
||||
def notify_order_completed(order = None, **kwargs):
|
||||
def notify_order_completed(order=None, **kwargs):
|
||||
if not order:
|
||||
raise ValueError("Order must be provided for notification.")
|
||||
|
||||
if kwargs:
|
||||
print("Additional kwargs received in notify_order_completed:", kwargs)
|
||||
|
||||
send_email_with_context(
|
||||
recipients=order.email,
|
||||
subject="Your order has been completed",
|
||||
raise ValueError("Order must be provided.")
|
||||
_notify_order(
|
||||
order,
|
||||
title="Objednávka dokončena",
|
||||
text=f"Vaše objednávka #{order.id} byla úspěšně dokončena. Děkujeme za nákup!",
|
||||
template_path="email/order_completed.html",
|
||||
context={
|
||||
"order": order,
|
||||
})
|
||||
)
|
||||
|
||||
0
backend/notifications/__init__.py
Normal file
0
backend/notifications/__init__.py
Normal file
11
backend/notifications/admin.py
Normal file
11
backend/notifications/admin.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from .models import Notification
|
||||
|
||||
|
||||
@admin.register(Notification)
|
||||
class NotificationAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "title", "notification_type", "user", "is_read", "bulk", "send_email", "created_at")
|
||||
list_filter = ("notification_type", "is_read", "bulk", "send_email", "created_at")
|
||||
search_fields = ("title", "text", "user__email", "user__username")
|
||||
ordering = ("-created_at",)
|
||||
readonly_fields = ("created_at", "read_at", "email_subject", "email_template_path", "send_email", "bulk")
|
||||
5
backend/notifications/apps.py
Normal file
5
backend/notifications/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NotificationsConfig(AppConfig):
|
||||
name = 'notifications'
|
||||
93
backend/notifications/consumers.py
Normal file
93
backend/notifications/consumers.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import json
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def _mark_notification_read(notification_id, user):
|
||||
from .models import Notification
|
||||
try:
|
||||
n = Notification.objects.get(pk=notification_id, user=user)
|
||||
if not n.is_read:
|
||||
n.is_read = True
|
||||
n.read_at = timezone.now()
|
||||
n.save(update_fields=["is_read", "read_at"])
|
||||
return n.read_at.isoformat()
|
||||
except Notification.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def _mark_all_notifications_read(user):
|
||||
from .models import Notification
|
||||
now = timezone.now()
|
||||
return Notification.objects.filter(user=user, is_read=False).update(is_read=True, read_at=now)
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def _delete_notification(notification_id, user):
|
||||
from .models import Notification
|
||||
try:
|
||||
n = Notification.objects.get(pk=notification_id, user=user)
|
||||
n.delete()
|
||||
return True
|
||||
except Notification.DoesNotExist:
|
||||
return False
|
||||
|
||||
|
||||
class NotificationConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
async def connect(self):
|
||||
user = self.scope["user"]
|
||||
if not user.is_authenticated:
|
||||
await self.close(code=4401)
|
||||
return
|
||||
self.group_name = f"notifications_{user.pk}"
|
||||
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
if hasattr(self, "group_name"):
|
||||
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
||||
|
||||
async def receive(self, text_data):
|
||||
data = json.loads(text_data)
|
||||
msg_type = data.get("type")
|
||||
|
||||
if msg_type == "mark_read":
|
||||
notification_id = data.get("id")
|
||||
read_at = await _mark_notification_read(notification_id, self.scope["user"])
|
||||
if read_at:
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "notification.read",
|
||||
"id": notification_id,
|
||||
"read_at": read_at,
|
||||
}))
|
||||
|
||||
elif msg_type == "mark_all_read":
|
||||
count = await _mark_all_notifications_read(self.scope["user"])
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "notification.read_all",
|
||||
"marked": count,
|
||||
}))
|
||||
|
||||
elif msg_type == "delete":
|
||||
notification_id = data.get("id")
|
||||
deleted = await _delete_notification(notification_id, self.scope["user"])
|
||||
if deleted:
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "notification.deleted",
|
||||
"id": notification_id,
|
||||
}))
|
||||
|
||||
async def notification_new(self, event):
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "notification.new",
|
||||
"id": event["id"],
|
||||
"title": event["title"],
|
||||
"text": event["text"],
|
||||
"notification_type": event["notification_type"],
|
||||
"action_url": event.get("action_url"),
|
||||
"created_at": event["created_at"],
|
||||
}))
|
||||
0
backend/notifications/migrations/__init__.py
Normal file
0
backend/notifications/migrations/__init__.py
Normal file
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
|
||||
6
backend/notifications/routing.py
Normal file
6
backend/notifications/routing.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r"ws/notifications/$", consumers.NotificationConsumer.as_asgi()),
|
||||
]
|
||||
15
backend/notifications/serializers.py
Normal file
15
backend/notifications/serializers.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Notification
|
||||
|
||||
|
||||
class NotificationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Notification
|
||||
fields = [
|
||||
'id', 'title', 'text', 'notification_type', 'action_url',
|
||||
'is_read', 'read_at', 'created_at', 'bulk', 'send_email',
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'title', 'text', 'notification_type', 'action_url',
|
||||
'is_read', 'read_at', 'created_at', 'bulk', 'send_email',
|
||||
]
|
||||
119
backend/notifications/tasks.py
Normal file
119
backend/notifications/tasks.py
Normal 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
|
||||
}
|
||||
3
backend/notifications/tests.py
Normal file
3
backend/notifications/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
backend/notifications/urls.py
Normal file
10
backend/notifications/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'', views.NotificationViewSet, basename='notification')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
51
backend/notifications/views.py
Normal file
51
backend/notifications/views.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import logging
|
||||
from django.utils import timezone
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
from .models import Notification
|
||||
from .serializers import NotificationSerializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@extend_schema(tags=["notifications"])
|
||||
class NotificationViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||
serializer_class = NotificationSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Notification.objects.filter(user=self.request.user)
|
||||
|
||||
@extend_schema(
|
||||
summary="Unread notification count",
|
||||
responses={200: {"type": "object", "properties": {"unread_count": {"type": "integer"}}}},
|
||||
)
|
||||
@action(detail=False, methods=["get"], url_path="unread-count")
|
||||
def unread_count(self, request):
|
||||
count = self.get_queryset().filter(is_read=False).count()
|
||||
return Response({"unread_count": count})
|
||||
|
||||
@extend_schema(summary="Mark a single notification as read", responses={200: NotificationSerializer})
|
||||
@action(detail=True, methods=["post"], url_path="read")
|
||||
def mark_read(self, request, pk=None):
|
||||
notification = self.get_object()
|
||||
if not notification.is_read:
|
||||
notification.is_read = True
|
||||
notification.read_at = timezone.now()
|
||||
notification.save(update_fields=["is_read", "read_at"])
|
||||
return Response(NotificationSerializer(notification).data)
|
||||
|
||||
@extend_schema(
|
||||
summary="Mark all notifications as read",
|
||||
responses={200: {"type": "object", "properties": {"marked": {"type": "integer"}}}},
|
||||
)
|
||||
@action(detail=False, methods=["post"], url_path="read-all")
|
||||
def mark_all_read(self, request):
|
||||
now = timezone.now()
|
||||
updated = self.get_queryset().filter(is_read=False).update(is_read=True, read_at=now)
|
||||
return Response({"marked": updated})
|
||||
1
backend/social/blog/IDEA.md
Normal file
1
backend/social/blog/IDEA.md
Normal file
@@ -0,0 +1 @@
|
||||
# editorjs.io - základ
|
||||
0
backend/social/blog/__init__.py
Normal file
0
backend/social/blog/__init__.py
Normal file
3
backend/social/blog/admin.py
Normal file
3
backend/social/blog/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
backend/social/blog/apps.py
Normal file
5
backend/social/blog/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
name = 'blog'
|
||||
0
backend/social/blog/migrations/__init__.py
Normal file
0
backend/social/blog/migrations/__init__.py
Normal file
16
backend/social/blog/models.py
Normal file
16
backend/social/blog/models.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Blog(models.Model):
|
||||
title = models.CharField(max_length=255)
|
||||
content = models.JsonField()
|
||||
|
||||
authors = models.ManyToManyField('Author', related_name='blogs')
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
0
backend/social/blog/serializers.py
Normal file
0
backend/social/blog/serializers.py
Normal file
3
backend/social/blog/tests.py
Normal file
3
backend/social/blog/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
backend/social/blog/views.py
Normal file
3
backend/social/blog/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
6
backend/social/chat/IDEA.md
Normal file
6
backend/social/chat/IDEA.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Notify requests for chats
|
||||
|
||||
# Notify @mentions in chats
|
||||
|
||||
# Add requests before filling chat (maybe add chat type request and allowed and archived for advanced filtering) + actions
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
chat app diagram
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CLIENT (browser) │
|
||||
└────────────┬────────────────────────────┬───────────────────────┘
|
||||
│ WebSocket │ HTTP REST
|
||||
│ ws/chat/<id>/ │ /api/social/
|
||||
▼ ▼
|
||||
┌────────────────────────┐ ┌────────────────────────────────────┐
|
||||
│ ChatConsumer │ │ REST Views │
|
||||
│ (consumers.py) │ │ (views.py) │
|
||||
│ │ │ │
|
||||
│ connect() │ │ ChatViewSet │
|
||||
│ ├─ auth check │ │ ├─ list / retrieve (GET) │
|
||||
│ └─ membership check ──┼───┼──► IsChatMember │
|
||||
│ │ │ ├─ create (POST) │
|
||||
│ receive() │ │ ├─ update/partial (PATCH/PUT) │
|
||||
│ ├─ new_chat_message │ │ │ └─ CanManageChat │
|
||||
│ │ (text only) │ │ ├─ destroy (DELETE) │
|
||||
│ │ └─► _create_message │ │ └─ CanManageChat │
|
||||
│ │ (DB INSERT) │ │ └─ add/remove member & moderator │
|
||||
│ │ │ │ │
|
||||
│ ├─ new_reply_message │ │ MessageViewSet │
|
||||
│ │ (text only) │ │ ├─ list / retrieve (GET) │
|
||||
│ │ └─► _create_message │ │ └─ IsAuthenticated │
|
||||
│ │ (DB INSERT) │ │ ├─ send (POST) │
|
||||
│ │ │ │ │ ├─ IsAuthenticated │
|
||||
│ └─ reaction │ │ │ ├─ DB INSERT Message │
|
||||
│ └─► _toggle_reaction │ │ ├─ DB INSERT MessageFile(s) │
|
||||
│ (DB UPDATE/ │ │ │ └─► group_send(chat.msg) ─────┼──► WebSocket push
|
||||
│ DELETE) │ │ ├─ update/partial (PATCH/PUT) │
|
||||
│ │ │ │ ├─ IsMessageSenderOnly │
|
||||
│ typing / stop_typing │ │ │ ├─ message.edit_content() │
|
||||
│ └─► group_send only │ │ │ └─► group_send(edit.msg) ─────┼──► WebSocket push
|
||||
│ (no DB write) │ │ └─ destroy (DELETE) │
|
||||
│ │ │ ├─ CanDeleteMessage │
|
||||
│ │ │ └─► group_send(delete.msg) ───┼──► WebSocket push
|
||||
└────────────┬───────────┘ └──────────────┬─────────────────────┘
|
||||
│ │
|
||||
│ both use channel_layer │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Channel Layer (Redis) │
|
||||
│ group: "chat_{id}" │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ group_send dispatches to all connected consumers
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ChatConsumer event handlers (push to each connected client) │
|
||||
│ chat_message · reply_chat_message · edit_message │
|
||||
│ delete_message · message_reaction · typing_status · stop_typing│
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────┐ ┌──────────────────────────┐
|
||||
│ Database writes summary │ │ When to use which path │
|
||||
│ │ │ │
|
||||
│ CREATE Message ← consumer │ │ WS ← text-only msg │
|
||||
│ (text only) │ │ HTTP← msg with files │
|
||||
│ CREATE Message ← view /send │ │ HTTP← edit message │
|
||||
│ (any) │ │ HTTP← delete message │
|
||||
│ CREATE MessageFile ← view/send │ │ WS ← reaction │
|
||||
│ UPDATE Message ← view │ │ WS ← typing indicator │
|
||||
│ DELETE Message ← view (soft) │ └──────────────────────────┘
|
||||
│ CREATE MessageReaction ← cons │
|
||||
│ UPDATE MessageReaction ← cons │
|
||||
│ DELETE MessageReaction ← cons │
|
||||
│ CREATE MessageHistory ← model │
|
||||
│ (auto on edit_content) │
|
||||
└──────────────────────────────────┘
|
||||
3
backend/social/hubs/IDEA.md
Normal file
3
backend/social/hubs/IDEA.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Notify on mod ADD
|
||||
|
||||
# notify for Apeal
|
||||
11
backend/social/posts/IDEA.md
Normal file
11
backend/social/posts/IDEA.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Votes in posts as widget
|
||||
|
||||
# Location tag
|
||||
|
||||
# PFP from hub on posts outside of hub
|
||||
|
||||
# Collab/multiple authors on posts
|
||||
|
||||
# NSFW tag, spoiler tag
|
||||
|
||||
# Notify on likes 1 --> 10 --> 100 -> 500 -> 1000 etc..
|
||||
@@ -18,9 +18,10 @@ django_asgi_app = get_asgi_application()
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from social.chat.routing import websocket_urlpatterns as social_ws
|
||||
from thirdparty.downloader.routing import websocket_urlpatterns as downloader_ws
|
||||
from notifications.routing import websocket_urlpatterns as notifications_ws
|
||||
from vontor_cz.middleware import JWTAuthMiddleware
|
||||
|
||||
websocket_urlpatterns = downloader_ws + social_ws
|
||||
websocket_urlpatterns = downloader_ws + social_ws + notifications_ws
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": django_asgi_app,
|
||||
|
||||
@@ -348,6 +348,7 @@ MY_CREATED_APPS = [
|
||||
'social.posts',
|
||||
|
||||
'advertisement',
|
||||
'notifications',
|
||||
|
||||
'thirdparty.downloader',
|
||||
'thirdparty.stripe', # register Stripe app so its models are recognized
|
||||
|
||||
@@ -43,6 +43,7 @@ urlpatterns = [
|
||||
path('api/configuration/', include('configuration.urls')),
|
||||
path('api/advertisement/', include('advertisement.urls')),
|
||||
|
||||
path('api/notifications/', include('notifications.urls')),
|
||||
path('api/social/hubs/', include('social.hubs.urls')),
|
||||
path('api/social/posts/', include('social.posts.urls')),
|
||||
path('api/social/', include('social.chat.urls')),
|
||||
|
||||
Reference in New Issue
Block a user