huge updates

This commit is contained in:
sankalp committed 2025-04-22 03:47:31 +05:45
1 parent b5a43ac0ad
commit 6cc74a355f
40 files changed
+6878 -2378

No files matched your search

+134 -81
View File
@@ -1,10 +1,17 @@
import os
import time
import logging
from typing import List, Tuple, Optional
from django.http import HttpResponse, JsonResponse
from django.db import models
from django.conf import settings
import firebase_admin
from firebase_admin import messaging, credentials
from firebase_admin import messaging, credentials, exceptions
from student_management_system.settings import BASE_DIR
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
# Configure logging
logger = logging.getLogger(__name__)
# --------------------------------------------------------------------
# FCM Device Model
@@ -40,9 +47,11 @@ class FCMDevice(models.Model):
verbose_name_plural = "FCM Devices"
ordering = ["-last_active"]
def deactivate(self):
"""Deactivate this device token"""
def deactivate(self, reason: str = None):
"""Deactivate this device token with optional reason"""
self.is_active = False
if reason:
logger.info(f"Deactivating device token {self.id}: {reason}")
self.save()
@@ -92,85 +101,138 @@ def save_fcm_token(request, token):
# --------------------------------------------------------------------
def get_firebase_js():
"""Generate Firebase JavaScript configuration"""
firebase_config = {
"apiKey": os.getenv("FIREBASE_API_KEY", ""),
"authDomain": os.getenv("FIREBASE_AUTH_DOMAIN", ""),
"projectId": os.getenv("FIREBASE_PROJECT_ID", ""),
"storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET", ""),
"messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""),
"appId": os.getenv("FIREBASE_APP_ID", ""),
"measurementId": os.getenv("FIREBASE_MEASUREMENT_ID", ""),
}
class FirebaseConfig:
"""Class to handle Firebase configuration and validation"""
@staticmethod
def validate_config():
"""Validate Firebase configuration"""
required_vars = [
'FIREBASE_API_KEY',
'FIREBASE_AUTH_DOMAIN',
'FIREBASE_PROJECT_ID',
'FIREBASE_STORAGE_BUCKET',
'FIREBASE_MESSAGING_SENDER_ID',
'FIREBASE_APP_ID'
]
missing_vars = [var for var in required_vars if not os.getenv(var)]
if missing_vars:
raise ValueError(f"Missing required Firebase configuration variables: {', '.join(missing_vars)}")
cert_path = os.path.join(BASE_DIR, "firebase-key.json")
if not os.path.exists(cert_path):
raise FileNotFoundError("Firebase service account key file not found")
data = """
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-messaging-compat.js');
firebase.initializeApp({
apiKey: "%s",
authDomain: "%s",
projectId: "%s",
storageBucket: "%s",
messagingSenderId: "%s",
appId: "%s",
measurementId: "%s"
});
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function(payload) {
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: payload.notification.icon || '/static/img/logo.png'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});
""" % (
firebase_config["apiKey"],
firebase_config["authDomain"],
firebase_config["projectId"],
firebase_config["storageBucket"],
firebase_config["messagingSenderId"],
firebase_config["appId"],
firebase_config["measurementId"],
)
return HttpResponse(data, content_type="application/javascript")
@staticmethod
def get_config():
"""Get validated Firebase configuration"""
FirebaseConfig.validate_config()
return {
"apiKey": os.getenv("FIREBASE_API_KEY"),
"authDomain": os.getenv("FIREBASE_AUTH_DOMAIN"),
"projectId": os.getenv("FIREBASE_PROJECT_ID"),
"storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET"),
"messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID"),
"appId": os.getenv("FIREBASE_APP_ID"),
"measurementId": os.getenv("FIREBASE_MEASUREMENT_ID", ""),
}
# --------------------------------------------------------------------
# Firebase Admin Initialization
# --------------------------------------------------------------------
# Initialize Firebase Admin
firebase_app = None
try:
cert_path = os.path.join(BASE_DIR, "firebase-key.json")
if os.path.exists(cert_path):
def initialize_firebase():
"""Initialize Firebase Admin with proper error handling"""
global firebase_app
try:
FirebaseConfig.validate_config()
cert_path = os.path.join(BASE_DIR, "firebase-key.json")
cred = credentials.Certificate(cert_path)
firebase_app = firebase_admin.initialize_app(cred)
except Exception:
pass
logger.info("Firebase Admin initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Firebase Admin: {str(e)}")
firebase_app = None
# Initialize Firebase
firebase_app = None
initialize_firebase()
# --------------------------------------------------------------------
# Push Notification Functions
# --------------------------------------------------------------------
class FirebaseQuotaManager:
"""Class to manage Firebase quota limits"""
def __init__(self):
self.quota_limit = 500 # Default quota limit per minute
self.requests_count = 0
self.last_reset_time = time.time()
def check_quota(self) -> bool:
"""Check if we're within quota limits"""
current_time = time.time()
if current_time - self.last_reset_time >= 60: # Reset every minute
self.requests_count = 0
self.last_reset_time = current_time
return self.requests_count < self.quota_limit
def increment_count(self):
"""Increment request count"""
self.requests_count += 1
def send_push_notification(title, message, tokens):
quota_manager = FirebaseQuotaManager()
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type((exceptions.FirebaseError, exceptions.UnknownError))
)
def send_single_notification(token: str, title: str, message: str) -> bool:
"""Send a single notification with retry mechanism"""
if not quota_manager.check_quota():
logger.warning("Firebase quota limit reached")
return False
try:
messaging.send(
messaging.Message(
notification=messaging.Notification(
title=title,
body=message,
),
token=token,
)
)
quota_manager.increment_count()
return True
except messaging.UnregisteredError:
logger.info(f"Token {token} is no longer valid")
return False
except exceptions.FirebaseError as e:
logger.error(f"Firebase error for token {token}: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error for token {token}: {str(e)}")
return False
def send_push_notification(title: str, message: str, tokens: List[str]) -> Tuple[int, int, List[str]]:
"""
Send push notification using Firebase Cloud Messaging
Send push notification using Firebase Cloud Messaging with improved error handling
Returns a tuple of (success_count, failure_count, failed_tokens)
"""
if not firebase_app or not tokens:
if not firebase_app:
logger.error("Firebase Admin not initialized")
return 0, len(tokens) if tokens else 0, tokens
if not isinstance(tokens, (list, tuple)):
tokens = list(tokens) # Convert QuerySet to list
tokens = list(tokens)
success_count = 0
failure_count = 0
@@ -180,42 +242,33 @@ def send_push_notification(title, message, tokens):
active_devices = FCMDevice.objects.filter(
token__in=tokens,
is_active=True,
is_fallback=False, # Skip fallback tokens for push notifications
is_fallback=False,
)
# Count fallback tokens as "success" for reporting purposes
fallback_count = FCMDevice.objects.filter(
token__in=tokens, is_active=True, is_fallback=True
).count()
success_count += fallback_count
for device in active_devices:
try:
# Skip tokens that match our fallback pattern
if device.token.startswith("fcm-token-") or device.token.startswith(
"fallback-token-"
):
if device.token.startswith(("fcm-token-", "fallback-token-")):
device.is_fallback = True
device.save()
continue
messaging.send(
messaging.Message(
notification=messaging.Notification(
title=title,
body=message,
),
token=str(device.token), # Ensure token is a string
)
)
success_count += 1
except messaging.UnregisteredError:
# Token is no longer valid, deactivate the device
device.deactivate()
failure_count += 1
failed_tokens.append(device.token)
except Exception:
if send_single_notification(device.token, title, message):
success_count += 1
else:
device.deactivate("Failed to send notification after retries")
failure_count += 1
failed_tokens.append(device.token)
except Exception as e:
logger.error(f"Error processing device {device.id}: {str(e)}")
device.deactivate(f"Error: {str(e)}")
failure_count += 1
failed_tokens.append(device.token)