import datetime import logging import celery from django.conf import settings from django.db import transaction from django.db.utils import IntegrityError from django.utils import timezone from .models import ( PushNotification, Device, get_filtered_devices_queryset ) from .exceptions import ( PushInvalidTokenException, PushException ) from .dispatchers import get_dispatcher logger = logging.getLogger(__name__) @celery.shared_task( queue=getattr(settings, 'PUSHY_QUEUE_DEFAULT_NAME', None) ) def check_pending_push_notifications(): pending_notifications = PushNotification.objects.filter( sent=PushNotification.PUSH_NOT_SENT ) for pending_notification in pending_notifications: create_push_notification_groups.apply_async(kwargs={ 'notification': pending_notification.to_dict() }) @celery.shared_task( queue=getattr(settings, 'PUSHY_QUEUE_DEFAULT_NAME', None) ) def create_push_notification_groups(notification): devices = get_filtered_devices_queryset(notification) date_started = timezone.now() if devices.count() > 0: count = devices.count() limit = getattr(settings, 'PUSHY_DEVICE_KEY_LIMIT', 1000) celery.chord( send_push_notification_group.s(notification, offset, limit) for offset in range(0, count, limit) )(notify_push_notification_sent.si(notification)) if not notification['id']: return try: notification = PushNotification.objects.get(pk=notification['id']) notification.sent = PushNotification.PUSH_IN_PROGRESS notification.date_started = date_started notification.save() except PushNotification.DoesNotExist: return @celery.shared_task( queue=getattr(settings, 'PUSHY_QUEUE_DEFAULT_NAME', None) ) def send_push_notification_group(notification, offset=0, limit=1000): devices = get_filtered_devices_queryset(notification) devices = devices[offset:offset + limit] for device in devices: send_single_push_notification(device, notification['payload']) return True @celery.shared_task( queue=getattr(settings, 'PUSHY_QUEUE_DEFAULT_NAME', None) ) def send_single_push_notification(device, payload): # The task can be called in two ways: # 1) from send_push_notification_group directly with a device instance # 2) As a task using .delay or apply_async with a device id if isinstance(device, int): try: device = Device.objects.get(pk=device) except Device.DoesNotExist: return False dispatcher = get_dispatcher(device.type) try: canonical_id = dispatcher.send(device.key, payload) if not canonical_id: return with transaction.atomic(): device.key = canonical_id device.save() except IntegrityError: device.delete() except PushInvalidTokenException: logger.debug('Token for device {} does not exist, skipping'.format( device.id )) device.delete() except PushException: logger.exception("An error occured while sending push notification") return @celery.shared_task( queue=getattr(settings, 'PUSH_QUEUE_DEFAULT_NAME', None), ) def notify_push_notification_sent(notification): if not notification['id']: return False try: notification = PushNotification.objects.get(pk=notification['id']) notification.date_finished = timezone.now() notification.sent = PushNotification.PUSH_SENT notification.save() except PushNotification.DoesNotExist: logger.exception("Notification {} does not exist".format(notification)) return False @celery.shared_task( queue=getattr(settings, 'PUSH_QUEUE_DEFAULT_NAME', None) ) def clean_sent_notifications(): max_age = getattr(settings, 'PUSHY_NOTIFICATION_MAX_AGE', None) if not max_age or not isinstance(max_age, datetime.timedelta): raise ValueError('Notification max age value is not defined.') delete_before_date = timezone.now() - max_age PushNotification.objects.filter( sent=PushNotification.PUSH_SENT, date_finished__lt=delete_before_date ).delete()