fix notification issue

This commit is contained in:
ismail 2025-09-17 15:44:47 +03:00
parent 536bdc4cb9
commit 10d48ca47d
5 changed files with 52 additions and 127 deletions

View File

@ -35,9 +35,9 @@ from pathlib import Path
# } # }
# ) # )
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
django.setup() # django.setup()
BASE_DIR = Path(__file__).resolve().parent.parent # BASE_DIR = Path(__file__).resolve().parent.parent
app = get_asgi_application() app = get_asgi_application()
# app = WhiteNoise(app, root=str(BASE_DIR / 'staticfiles')) # app = WhiteNoise(app, root=str(BASE_DIR / 'staticfiles'))
@ -50,7 +50,7 @@ application = ProtocolTypeRouter(
path("sse/notifications/", NotificationSSEApp()), path("sse/notifications/", NotificationSSEApp()),
re_path( re_path(
r"", app r"", app
), # All other routes go to Django ),
] ]
) )
), ),
@ -58,5 +58,5 @@ application = ProtocolTypeRouter(
) )
if django.conf.settings.DEBUG: # if django.conf.settings.DEBUG:
application = ASGIStaticFilesHandler(app) # application = ASGIStaticFilesHandler(app)

View File

@ -5,7 +5,7 @@ from django.urls import path, include
from schema_graph.views import Schema from schema_graph.views import Schema
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from inventory.notifications.sse import NotificationSSEApp # from inventory.notifications.sse import NotificationSSEApp
# import debug_toolbar # import debug_toolbar
# from two_factor.urls import urlpatterns as tf_urls # from two_factor.urls import urlpatterns as tf_urls

View File

@ -1,82 +1,3 @@
# import json
# from django.contrib.auth.models import AnonymousUser
# from django.contrib.auth import get_user_model
# from django.db import close_old_connections
# from urllib.parse import parse_qs
# from channels.db import database_sync_to_async
# from inventory.models import Notification
# import asyncio
# @database_sync_to_async
# def get_notifications(user, last_id):
# return Notification.objects.filter(
# user=user, id__gt=last_id, is_read=False
# ).order_by("created")
# class NotificationSSEApp:
# async def __call__(self, scope, receive, send):
# if scope["type"] != "http":
# return
# query_string = parse_qs(scope["query_string"].decode())
# last_id = int(query_string.get("last_id", [0])[0])
# # Get user from scope if using AuthMiddlewareStack
# user = scope.get("user", AnonymousUser())
# if not user.is_authenticated:
# await send({
# "type": "http.response.start",
# "status": 403,
# "headers": [(b"content-type", b"text/plain")],
# })
# await send({
# "type": "http.response.body",
# "body": b"Unauthorized",
# })
# return
# await send({
# "type": "http.response.start",
# "status": 200,
# "headers": [
# (b"content-type", b"text/event-stream"),
# (b"cache-control", b"no-cache"),
# (b"x-accel-buffering", b"no"),
# ]
# })
# try:
# while True:
# close_old_connections()
# notifications = await get_notifications(user, last_id)
# for notification in notifications:
# data = {
# "id": notification.id,
# "message": notification.message,
# "created": notification.created.isoformat(),
# "is_read": notification.is_read,
# }
# event_str = (
# f"id: {notification.id}\n"
# f"event: notification\n"
# f"data: {json.dumps(data)}\n\n"
# )
# await send({
# "type": "http.response.body",
# "body": event_str.encode("utf-8"),
# "more_body": True
# })
# last_id = notification.id
# await asyncio.sleep(2)
# except asyncio.CancelledError:
# pass
import json import json
import time import time
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser

View File

@ -5120,28 +5120,28 @@ class EstimatePrintView(EstimateDetailView):
uses a dedicated, stripped-down print template. uses a dedicated, stripped-down print template.
""" """
template_name = "sales/estimates/estimate_preview.html" template_name = "sales/estimates/estimate_preview.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
context = self.get_context_data(object=self.object) context = self.get_context_data(object=self.object)
# lang = request.GET.get('lang', 'ar') # lang = request.GET.get('lang', 'ar')
template_path = "sales/estimates/estimate_preview.html" template_path = "sales/estimates/estimate_preview.html"
html_string = render_to_string(template_path, context) html_string = render_to_string(template_path, context)
pdf_file = HTML(string=html_string).write_pdf() pdf_file = HTML(string=html_string).write_pdf()
response = HttpResponse(pdf_file, content_type='application/pdf') response = HttpResponse(pdf_file, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="estimate_{self.object.estimate_number}.pdf"' response['Content-Disposition'] = f'attachment; filename="estimate_{self.object.estimate_number}.pdf"'
return response return response
@ -10238,7 +10238,8 @@ def payment_callback(request, dealer_slug):
# return render(request, "payment_failed.html", {"message": message}) # return render(request, "payment_failed.html", {"message": message})
@login_required @login_required
async def sse_stream(request): # 👈 Mark as async! async def sse_stream(request):
import asyncio
def event_generator(): def event_generator():
last_id = int(request.GET.get("last_id", 0)) last_id = int(request.GET.get("last_id", 0))
@ -11014,18 +11015,18 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai
"item_model", "bill_model" "item_model", "bill_model"
) )
) )
if self.object.po_status == 'fulfilled': if self.object.po_status == 'fulfilled':
context['po_items_list']=po_items_qs context['po_items_list']=po_items_qs
context['vendor']=po_items_qs.first().bill_model.vendor context['vendor']=po_items_qs.first().bill_model.vendor
context['dealer']=request.dealer context['dealer']=request.dealer
# Check if PDF format is requested # Check if PDF format is requested
if request.GET.get('format') == 'pdf': if request.GET.get('format') == 'pdf':
# Use a separate, print-friendly template for the PDF # Use a separate, print-friendly template for the PDF
if request.GET.get('lang')=='en': if request.GET.get('lang')=='en':
html_string = render_to_string( html_string = render_to_string(
"purchase_orders/po_detail_en_pdf.html", "purchase_orders/po_detail_en_pdf.html",
context context
) )
else: else:
@ -11033,12 +11034,12 @@ class PurchaseOrderDetailView(LoginRequiredMixin, PermissionRequiredMixin, Detai
"purchase_orders/po_detail_ar_pdf.html", "purchase_orders/po_detail_ar_pdf.html",
context context
) )
# Use WeasyPrint to generate the PDF # Use WeasyPrint to generate the PDF
pdf = HTML(string=html_string).write_pdf() pdf = HTML(string=html_string).write_pdf()
response = HttpResponse(pdf, content_type="application/pdf") response = HttpResponse(pdf, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="PO_{self.object.po_number}.pdf"' response["Content-Disposition"] = f'attachment; filename="PO_{self.object.po_number}.pdf"'
return response return response

View File

@ -87,9 +87,13 @@
didOpen: (toast) => { didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer; toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer; toast.onmouseleave = Swal.resumeTimer;
} }); }
});
{% with last_notif=notifications_|last %}
let lastNotificationId = {{ last_notif.id|default:0 }};
{% endwith %}
let lastNotificationId = {{ notifications_.last.id|default:0 }};
let seenNotificationIds = new Set(); let seenNotificationIds = new Set();
let counter = document.getElementById('notification-counter'); let counter = document.getElementById('notification-counter');
let notificationsContainer = document.getElementById('notifications-container'); let notificationsContainer = document.getElementById('notifications-container');
@ -100,7 +104,6 @@
let initialUnreadCount = {{ notifications_.count|default:0 }}; let initialUnreadCount = {{ notifications_.count|default:0 }};
updateCounter(initialUnreadCount); updateCounter(initialUnreadCount);
fetchInitialNotifications(); fetchInitialNotifications();
function fetchInitialNotifications() { function fetchInitialNotifications() {
@ -108,29 +111,22 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.notifications && data.notifications.length > 0) { if (data.notifications && data.notifications.length > 0) {
lastNotificationId = data.notifications[0].id; lastNotificationId = data.notifications[0].id;
seenNotificationIds = new Set(); seenNotificationIds = new Set();
let unreadCount = 0; let unreadCount = 0;
data.notifications.forEach(notification => { data.notifications.forEach(notification => {
seenNotificationIds.add(notification.id); seenNotificationIds.add(notification.id);
if (!notification.is_read) { if (!notification.is_read) unreadCount++;
unreadCount++;
}
}); });
renderNotifications(data.notifications); renderNotifications(data.notifications);
updateCounter(unreadCount); updateCounter(unreadCount);
setTimeout(() => {
connectSSE();
}, 5000);
} }
// Always connect SSE after initial load
setTimeout(() => {
connectSSE();
}, 1000);
}) })
.catch(error => { .catch(error => {
console.error('Error fetching initial notifications:', error); console.error('Error fetching initial notifications:', error);
@ -143,12 +139,12 @@
eventSource.close(); eventSource.close();
} }
// ✅ FIXED URL HERE
eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId); eventSource = new EventSource("/sse/notifications/?last_id=" + lastNotificationId);
eventSource.addEventListener('notification', function(e) { eventSource.addEventListener('notification', function(e) {
try { try {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (seenNotificationIds.has(data.id)) return; if (seenNotificationIds.has(data.id)) return;
seenNotificationIds.add(data.id); seenNotificationIds.add(data.id);
@ -158,6 +154,11 @@
updateCounter('increment'); updateCounter('increment');
if (!notificationsContainer) {
console.warn("Notification container missing, can't render SSE event");
return;
}
const notificationElement = createNotificationElement(data); const notificationElement = createNotificationElement(data);
notificationsContainer.insertAdjacentHTML('afterbegin', notificationElement); notificationsContainer.insertAdjacentHTML('afterbegin', notificationElement);
@ -168,7 +169,7 @@
Toast.fire({ Toast.fire({
icon: 'info', icon: 'info',
html:`${data.message}` html: `${data.message}`
}); });
} catch (error) { } catch (error) {
@ -220,7 +221,7 @@
</div> </div>
</div> </div>
</div> </div>
`; `;
} }
function updateCounter(action) { function updateCounter(action) {
@ -231,12 +232,14 @@
if (notificationCountDiv) { if (notificationCountDiv) {
notificationCountDiv.innerHTML = ` notificationCountDiv.innerHTML = `
<span class="badge bg-danger rounded-pill" id="notification-counter" style="position: absolute; top: 8px; right: 3px; font-size: 0.50rem;">0</span> <span class="badge bg-danger rounded-pill" id="notification-counter" style="position: absolute; top: 8px; right: 3px; font-size: 0.50rem;">0</span>
`; `;
counter = document.getElementById('notification-counter'); counter = document.getElementById('notification-counter');
} }
} }
} }
if (!counter) return;
let currentCount = parseInt(counter.textContent) || 0; let currentCount = parseInt(counter.textContent) || 0;
if (action === 'increment') { if (action === 'increment') {
@ -294,11 +297,11 @@
notificationCard.closest('.notification-card').classList.add('fade-out'); notificationCard.closest('.notification-card').classList.add('fade-out');
setTimeout(() => { setTimeout(() => {
notificationCard.closest('.notification-card').remove(); notificationCard.closest('.notification-card').remove();
}, 200); }, 1000);
} }
} }
}); });
} }
}); });
}); });
</script> </script>