Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)
2 changes: 2 additions & 0 deletions django_notification_system/models/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Notification(CreatedModifiedAbstractModel):
OPTED_OUT = "OPTED OUT"
RETRY = "RETRY"
SCHEDULED = "SCHEDULED"
ASYNCED = "ASYNCED"

STATUS_CHOICES = (
(DELIVERED, "Delivered"),
Expand All @@ -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)
Expand Down
109 changes: 109 additions & 0 deletions django_notification_system/utils/sender.py
Original file line number Diff line number Diff line change
@@ -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("*********************************")
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down