# Imports
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
import logging
from superdjango.db.lookups.models import StringLookupModel
from .constants import DEFAULT_ICONS, LEVEL, STAGE
log = logging.getLogger(__name__)
# Exports
__all__ = (
"Category",
"Notification",
"NotificationUser",
"UserPreferences",
)
# Constants
AUTH_USER_MODEL = settings.AUTH_USER_MODEL
NOTIFICATION_ICONS = getattr(settings, "SUPERDJANGO_NOTIFICATION_ICONS", DEFAULT_ICONS)
"""Load notification icons from settings. Useful, for example, to include pro icons."""
# Models
[docs]class Category(StringLookupModel):
"""Notification categories."""
class Meta:
ordering = ["label"]
verbose_name = _("Category")
verbose_name_plural = _("Categories")
[docs]class Notification(models.Model):
"""A notification sent to a user."""
action_text = models.CharField(
_("action text"),
blank=True,
help_text=_('The text to display for the action. Defaults to "More" in templates.'),
max_length=128,
null=True
)
action_url = models.CharField(
_("action URL"),
blank=True,
help_text=_("The URL to which users should be directed to take action or get more information."),
max_length=1024,
null=True
)
added_dt = models.DateTimeField(
_("added date/time"),
auto_now_add=True,
help_text=_("Date and time the notification was added."),
)
category = models.ForeignKey(
Category,
blank=True,
help_text=_("The category of the message."),
limit_choices_to={'is_enabled': True},
null=True,
on_delete=models.SET_NULL,
related_name="notifications",
verbose_name=_("category")
)
body = models.TextField(
_("body"),
help_text=_("The body of the message."),
)
body_sms = models.TextField(
_("SMS body"),
blank=True,
help_text=_("The SMS/text body of the message."),
null=True
)
email_enabled = models.BooleanField(
_("email enabled"),
default=False,
help_text=_("Indicates the message should be sent via email.")
)
has_been_sent = models.BooleanField(
_("sent"),
default=False,
help_text=_("Indicates the notification has been sent.")
)
html_enabled = models.BooleanField(
_("HTML enabled"),
default=False,
help_text=_("Indicates HTML email output should be supported.")
)
icon_override = models.CharField(
_("icon override"),
blank=True,
help_text=_("Override the default icon."),
max_length=128,
null=True
)
is_mandatory = models.BooleanField(
_("mandatory"),
default=False,
help_text=_("Indicates the notification should be sent regardless of user preferences.")
)
# https://www.codexworld.com/create-top-notification-bar-html-css-jquery/
is_promoted = models.BooleanField(
_("promoted"),
default=False,
help_text=_("Indicates the notification should be promoted to prominence.")
)
LEVEL_CHOICES = (
(LEVEL.DEBUG, _("Debug")),
(LEVEL.INFO, _("Info")),
(LEVEL.SUCCESS, _("Success")),
(LEVEL.WARNING, _("Warning")),
(LEVEL.ERROR, _("Error")),
)
level = models.PositiveSmallIntegerField(
_("level"),
choices=LEVEL_CHOICES,
default=LEVEL.INFO,
help_text=_("The level of the notification.")
)
sent_dt = models.DateTimeField(
_("sent date/time"),
blank=True,
help_text=_("Date and time the notification was sent."),
null=True
)
sms_enabled = models.BooleanField(
_("SMS enabled"),
default=False,
help_text=_("Indicates the message should be sent via SMS/text.")
)
STAGE_CHOICES = (
(STAGE.DRAFT, _("Draft")),
(STAGE.QUEUED, _("Queued")),
(STAGE.SENT, _("Sent")),
(STAGE.ARCHIVED, _("Archived")),
)
stage = models.PositiveSmallIntegerField(
_("stage"),
choices=STAGE_CHOICES,
default=STAGE.QUEUED,
help_text=_("The current stage of the notification.")
)
subject = models.CharField(
_("subject"),
help_text=_("The subject line of the message."),
max_length=128
)
users = models.ManyToManyField(
AUTH_USER_MODEL,
blank=True,
help_text=_("The users to which the notification is sent."),
related_name="sd_notifications",
verbose_name=_("users")
)
class Meta:
get_latest_by = "added_dt"
ordering = ["-added_dt"]
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
def __str__(self):
return self.subject
[docs] @classmethod
def create_for(cls, body, subject, users, action_text=None, action_url=None, body_sms=None, category=None,
email_enabled=False, html_enabled=False, icon_override=None, is_mandatory=False, is_promoted=False,
level=LEVEL.INFO, sms_enabled=False):
"""Create a notification for the given users.
:param body: Required. The body of the message.
:type body: str
:param subject: Required. The subject line of the message.
:type subject: str
:param users: An iterable of users to which the notification should be sent.
:param action_text: The action text to display for the URL. Defaults to "More" in templates.
:type action_text: str
:param action_url: Optional. The action URL.
:type action_url: str
:param body_sms: Optional. The SMS/text version of the message.
:type body_sms: str
:param category: The message category.
:type category: str | Category
:param email_enabled: Indicates email should be sent. Requires additional setup.
:type email_enabled: bool
:param html_enabled: Indicates the body of the message is HTML.
:type html_enabled: bool
:param icon_override: Overrides the default icon.
:type icon_override: str
:param is_mandatory: Indicates the message should be sent regardless of user preferences.
:type is_mandatory: bool
:param is_promoted: Indicates the message should be displayed with prominence.
:type is_promoted: bool
:param level: The level of the message.
:type level: int
:param sms_enabled: Indicates email should be sent. Requires additional setup.
:type sms_enabled: bool
:rtype: Notification
.. warning::
This creates a notification that is immediately queued for delivery.
"""
_category = None
if category is not None:
if isinstance(category, Category):
_category = category
else:
try:
_category = Category.objects.get(value=category)
except Category.DoesNotExist:
log.debug("Notification category does not exist: %s" % category)
notification = cls(
action_text=action_text,
action_url=action_url,
body=body,
body_sms=body_sms,
category=_category,
email_enabled=email_enabled,
html_enabled=html_enabled,
icon_override=icon_override,
is_mandatory=is_mandatory,
is_promoted=is_promoted,
level=level,
sms_enabled=sms_enabled,
subject=subject
)
notification.save()
for user in users:
notification.users.add(user)
# for user in users:
# link = NotificationUser(
# notification=notification,
# user=user
# )
# link.save()
return notification
[docs] def get_icon(self):
"""Get the Font Awesome icon associated with the level.
:rtype: str
"""
if self.icon_override:
return self.icon_override
return NOTIFICATION_ICONS[self.level]
@property
def icon(self):
"""Alias for ``get_icon()``."""
return self.get_icon()
[docs] def mark_sent(self, commit=True):
"""Mark the notification as sent.
:param commit: Save after updating the fields.
:type commit: bool
"""
self.has_been_sent = True
self.sent_dt = now()
self.stage = STAGE.SENT
if commit:
self.save(update_fields=["has_been_sent", "sent_dt", "stage"])
[docs]class NotificationUser(models.Model):
"""Track the users that have seen a notification."""
has_been_viewed = models.BooleanField(
_("viewed"),
default=False,
help_text=_("Indicates the notification has been viewed.")
)
notification = models.ForeignKey(
Notification,
help_text=_("The notification being tracked."),
on_delete=models.CASCADE,
related_name="user_views",
verbose_name=_("notification")
)
sent_email = models.NullBooleanField(
_("email sent"),
help_text=_("Indicates whether an email was sent for this notification.")
)
sent_sms = models.NullBooleanField(
_("SMS sent"),
help_text=_("Indicates whether a SMS/text was sent for this notification.")
)
user = models.ForeignKey(
AUTH_USER_MODEL,
help_text=_("The user to which the notification is sent."),
on_delete=models.CASCADE,
related_name="notifications",
verbose_name=_("user")
)
viewed_dt = models.DateTimeField(
_("viewed date/time"),
blank=True,
help_text=_("Date and time the notification was viewed."),
null=True
)
class Meta:
get_latest_by = "notification__added_dt"
ordering = ["-viewed_dt"]
unique_together = ["notification", "user"]
verbose_name = _("Notification View")
verbose_name_plural = _("Notification Views")
def __str__(self):
return "%s %s" % (self.notification.subject, self.user)
@property
def action_url(self):
"""Alias on ``notification``."""
return self.notification.action_url
@property
def added_dt(self):
"""Alias on ``notification``."""
return self.notification.added_dt
@property
def body(self):
"""Alias on ``notification``."""
return self.notification.body
@property
def body_sms(self):
"""Alias on ``notification``."""
return self.notification.body_sms
@property
def email_enabled(self):
"""Alias on ``notification``."""
return self.notification.email_enabled
@property
def has_been_sent(self):
"""Alias on ``notification``."""
return self.notification.has_been_sent
@property
def icon(self):
"""Alias on ``notification``."""
return self.notification.get_icon()
@property
def is_mandatory(self):
"""Alias on ``notification``."""
return self.notification.is_mandatory
@property
def is_promoted(self):
"""Alias on ``notification``."""
return self.notification.is_promoted
@property
def level(self):
"""Alias on ``notification``."""
return self.notification.level
[docs] def get_level_display(self):
"""Alias on ``notification``."""
return self.notification.get_level_display()
[docs] def mark_viewed(self, commit=True):
"""Mark the record as viewed.
:param commit: Indicates the record should be saved after updating the field.
:type commit: bool
"""
self.has_been_viewed = True
self.viewed_dt = now()
if commit:
self.save(update_fields=["has_been_viewed", "viewed_dt"])
[docs] def mark_unviewed(self, commit=True):
"""Mark the record as "un-viewed", removing the last viewed data.
:param commit: Indicates the record should be saved after updating the field.
:type commit: bool
"""
self.has_been_viewed = False
self.viewed_dt = None
if commit:
self.save(update_fields=["has_been_viewed", "viewed_dt"])
@property
def sent_dt(self):
"""Alias on ``notification``."""
return self.notification.sent_dt
@property
def sms_enabled(self):
"""Alias on ``notification``."""
return self.notification.sms_enabled
@property
def stage(self):
"""Alias on ``notification``."""
return self.notification.stage
@property
def subject(self):
"""Alias on ``notification``."""
return self.notification.subject
[docs]class UserPreferences(models.Model):
"""User notification preferences."""
categories = models.ManyToManyField(
Category,
blank=True,
help_text=_("The category or categories of notifications to be received."),
limit_choices_to={'is_enabled': True},
related_name="user_preferences",
verbose_name=_("categories")
)
email_enabled = models.BooleanField(
_("email enabled"),
default=True,
help_text=_("Indicates email notifications are enabled. Some mandatory messages may be sent even "
"when disabled.")
)
LEVEL_CHOICES = (
(LEVEL.DEBUG, _("Debug")),
(LEVEL.INFO, _("Info")),
(LEVEL.SUCCESS, _("Success")),
(LEVEL.WARNING, _("Warning")),
(LEVEL.ERROR, _("Error")),
)
level = models.PositiveSmallIntegerField(
_("level"),
choices=LEVEL_CHOICES,
default=LEVEL.ERROR,
help_text=_("The minimum level of the notifications to receive.")
)
mobile_number = models.CharField(
_("mobile number"),
blank=True,
help_text=_("The mobile number at which SMS/text notifications may be received. Note: Mobile charges "
"may apply."),
max_length=128,
null=True
)
sms_enabled = models.BooleanField(
_("SMS enabled"),
default=False,
help_text=_("Indicates messages may be sent via SMS/text.")
)
user = models.OneToOneField(
AUTH_USER_MODEL,
help_text=_("The user to which the preferences belong."),
on_delete=models.CASCADE,
related_name="notification_preferences",
verbose_name=_("user")
)
class Meta:
verbose_name = _("User Notification Preferences")
verbose_name_plural = _("User Notification Preferences")