diff --git a/django_notification_system/management/commands/process_notifications.py b/django_notification_system/management/commands/process_notifications.py index f3ca9f5..911c8d0 100644 --- a/django_notification_system/management/commands/process_notifications.py +++ b/django_notification_system/management/commands/process_notifications.py @@ -1,17 +1,6 @@ -import importlib -import inspect -import os -from os import path - -from django.conf import settings from django.core.management.base import BaseCommand -from django.db.models import Q -from django.utils import timezone -from ...models import Notification -from ...notification_handlers.email import send_notification as send_email -from ...notification_handlers.twilio import send_notification as send_twilio -from ...notification_handlers.expo import send_notification as send_expo +from ...utils.sender import Sender class Command(BaseCommand): @@ -21,77 +10,6 @@ class Command(BaseCommand): help = __doc__ - __function_table = { - "expo": send_expo, - "twilio": send_twilio, - "email": send_email - } - - @classmethod - def _load_function_table(cls): - """ - This function will get our function table populated with all available `send_notification` - functions. - """ - for directory in settings.NOTIFICATION_SYSTEM_HANDLERS: - try: - for file in os.listdir(path.join(directory)): - if "__init__" in file: - # Ignore the init file - continue - try: - # Create the module spec - module_spec = importlib.util.spec_from_file_location( - file, f"{directory}/{file}") - # Create a new module based on the spec - module = importlib.util.module_from_spec(module_spec) - # An abstract method that executes the module - module_spec.loader.exec_module(module) - # Get the actual function - real_func = getattr(module, "send_notification") - # Add it to our dictionary of functions - notification_system = file.partition(".py")[0] - cls.__function_table[notification_system] = real_func - except (ModuleNotFoundError, AttributeError): - pass - except FileNotFoundError: - # the directory provided in the settings file does not exist - pass - def handle(self, *args, **options): - # Load the function table - self._load_function_table() - - # Get all SCHEDULED and RETRY notifications with a - # scheduled_delivery before the current date_time - notifications = Notification.objects.filter( - Q(status="SCHEDULED") | Q(status="RETRY"), - scheduled_delivery__lte=timezone.now(), - ) - - # excludes all notifications where the user has NotificationOptOut object with has_opted_out=True - notifications = notifications.exclude(target_user_record__user__notification_opt_out__active=True) - - # Loop through each notification and attempt to push it - for notification in notifications: - print( - f"{notification.target_user_record.user.username} - {notification.scheduled_delivery} - {notification.status}") - print(f"{notification.title} - {notification.body}") - - if not notification.target_user_record.active: - notification.status = Notification.INACTIVE_DEVICE - notification.save() - else: - notification_type = ( - notification.target_user_record.target.notification_module_name - ) - try: - # Use our function table to call the appropriate sending function - response_message = self.__function_table[notification_type](notification) - except KeyError: - print( - f"invalid notification target name {notification.target_user_record.target.name}") - else: - # The notification was sent successfully - print(response_message) - print("*********************************") + sender = Sender() + sender.execute(verbose=True) diff --git a/django_notification_system/models/notification.py b/django_notification_system/models/notification.py index 57d4186..0b41454 100644 --- a/django_notification_system/models/notification.py +++ b/django_notification_system/models/notification.py @@ -45,6 +45,7 @@ class Notification(CreatedModifiedAbstractModel): OPTED_OUT = "OPTED OUT" RETRY = "RETRY" SCHEDULED = "SCHEDULED" + ASYNCED = "ASYNCED" STATUS_CHOICES = ( (DELIVERED, "Delivered"), @@ -53,6 +54,7 @@ class Notification(CreatedModifiedAbstractModel): (OPTED_OUT, "Opted Out"), (RETRY, "Retry"), (SCHEDULED, "Scheduled"), + (ASYNCED, "Asynced") ) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/django_notification_system/utils/sender.py b/django_notification_system/utils/sender.py new file mode 100644 index 0000000..51dfdfa --- /dev/null +++ b/django_notification_system/utils/sender.py @@ -0,0 +1,109 @@ +import importlib +import inspect +import os +from os import path + +from django.db.models import Q +from django.utils import timezone +from django.conf import settings +if settings.NOTIFICATION_SYSTEM_USE_CELERY: + from celery.execute import send_task + +from ..models import Notification +from ..notification_handlers.email import send_notification as send_email +from ..notification_handlers.twilio import send_notification as send_twilio +from ..notification_handlers.expo import send_notification as send_expo + + +class Sender(): + __function_table = { + "expo": send_expo, + "twilio": send_twilio, + "email": send_email + } + + def __init__(self, *args, **kwargs): + self._load_function_table() + + @classmethod + def _load_function_table(cls): + """ + This function will get our function table populated with all available `send_notification` + functions. + """ + for directory in settings.NOTIFICATION_SYSTEM_HANDLERS: + try: + for file in os.listdir(path.join(directory)): + if "__init__" in file: + # Ignore the init file + continue + try: + # Create the module spec + module_spec = importlib.util.spec_from_file_location( + file, f"{directory}/{file}") + # Create a new module based on the spec + module = importlib.util.module_from_spec(module_spec) + # An abstract method that executes the module + module_spec.loader.exec_module(module) + # Get the actual function + real_func = getattr(module, "send_notification") + # Add it to our dictionary of functions + notification_system = file.partition(".py")[0] + cls.__function_table[notification_system] = real_func + except (ModuleNotFoundError, AttributeError): + pass + except FileNotFoundError: + # the directory provided in the settings file does not exist + pass + + @classmethod + def send_async(cls, notification_type, notification_id): + sender = Sender() + notification = Notification.objects.get(id=notification_id) + sender.send_notification(notification_type, notification) + + + def send_notification(self, notification_type, notification): + return self.__function_table[notification_type](notification) + + def execute(self, verbose=False): + # Get all SCHEDULED and RETRY notifications with a + # scheduled_delivery before the current date_time + notifications = Notification.objects.filter( + Q(status="SCHEDULED") | Q(status="RETRY"), + scheduled_delivery__lte=timezone.now(), + ) + + # excludes all notifications where the user has NotificationOptOut object with has_opted_out=True + notifications = notifications.exclude(target_user_record__user__notification_opt_out__active=True) + + # Loop through each notification and attempt to push it + for notification in notifications: + if verbose: + print( + f"{notification.target_user_record.user.username} - {notification.scheduled_delivery} - {notification.status}") + print(f"{notification.title} - {notification.body}") + + if not notification.target_user_record.active: + notification.status = Notification.INACTIVE_DEVICE + notification.save() + else: + notification_type = ( + notification.target_user_record.target.notification_module_name + ) + try: + # Use our function table to call the appropriate sending function + if settings.NOTIFICATION_SYSTEM_USE_CELERY: + notification.status = "ASYNCED" + notification.save() + send_task("Sender.send_async", [notification_type, notification.id]) + else: + response_message = self.send_notification(notification_type, notification) + except KeyError: + if verbose: + print( + f"invalid notification target name {notification.target_user_record.target.name}") + elif verbose: + # The notification was sent successfully + print(response_message) + print("*********************************") diff --git a/setup.cfg b/setup.cfg index 1b4ec3a..b38e917 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = django-notification-system -version = 1.2 +version = 1.3 description = A Django app that allows developers to send notifications of various types to users. description-file = README.md long_description_content_type = text/markdown diff --git a/setup.py b/setup.py index 238042b..7b5ce96 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read(f): # For a discussion on single-sourcing the version across setup.py and the # project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='1.2.0', # Required + version='1.3.0', # Required # This is a one-line description or tagline of what your project does. This # corresponds to the "Summary" metadata field: