Restructured files

This commit is contained in:
Alvaro Mariano 2023-07-07 00:54:47 +00:00
parent f33afff505
commit c6b5a09610
17 changed files with 433 additions and 422 deletions

View file

@ -4,19 +4,11 @@ from django.contrib import admin
from django.utils.translation import gettext_lazy
from swapper import load_model
from notifications.base.admin import AbstractNotificationAdmin
Notification = load_model("notifications", "Notification")
def mark_unread(queryset, *args, **kwargs):
queryset.update(unread=True)
mark_unread.short_description = gettext_lazy("Mark selected notifications as unread")
class NotificationAdmin(AbstractNotificationAdmin):
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
raw_id_fields = ("recipient",)
readonly_fields = ("action_object_url", "actor_object_url", "target_object_url")
list_display = ("recipient", "actor", "level", "target", "unread", "public")
@ -26,11 +18,16 @@ class NotificationAdmin(AbstractNotificationAdmin):
"public",
"timestamp",
)
actions = [mark_unread]
actions = ("mark_unread", "mark_read")
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("actor")
@admin.action(description=gettext_lazy("Mark selected notifications as unread"))
def mark_unread(self, request, queryset):
queryset.update(unread=True)
admin.site.register(Notification, NotificationAdmin)
@admin.action(description=gettext_lazy("Mark selected notifications as read"))
def mark_read(self, request, queryset):
queryset.update(unread=False)

View file

@ -8,3 +8,12 @@ class Config(AppConfig):
name = "notifications"
verbose_name = _("Notifications")
default_auto_field = "django.db.models.AutoField"
def ready(self) -> None:
from notifications.signals import ( # pylint: disable=import-outside-toplevel
notify,
notify_handler,
)
notify.connect(notify_handler, dispatch_uid="notifications.models.notification")
return super().ready()

View file

@ -1,16 +0,0 @@
from django.contrib import admin
class AbstractNotificationAdmin(admin.ModelAdmin):
raw_id_fields = ("recipient",)
list_display = ("recipient", "actor", "level", "target", "unread", "public")
list_filter = (
"level",
"unread",
"public",
"timestamp",
)
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related("actor")

View file

@ -1,365 +0,0 @@
# -*- coding: utf-8 -*-
# pylint: disable=too-many-lines
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.query import QuerySet
from django.urls import NoReverseMatch, reverse
from django.utils import timesince, timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from swapper import load_model
from notifications import settings as notifications_settings
from notifications.signals import notify
EXTRA_DATA = notifications_settings.get_config()["USE_JSONFIELD"]
def is_soft_delete():
return notifications_settings.get_config()["SOFT_DELETE"]
def assert_soft_delete():
if not is_soft_delete():
# msg = """To use 'deleted' field, please set 'SOFT_DELETE'=True in settings.
# Otherwise NotificationQuerySet.unread and NotificationQuerySet.read do NOT filter by 'deleted' field.
# """
msg = "REVERTME"
raise ImproperlyConfigured(msg)
class NotificationQuerySet(models.query.QuerySet):
"""Notification QuerySet"""
def unsent(self):
return self.filter(emailed=False)
def sent(self):
return self.filter(emailed=True)
def unread(self, include_deleted=False):
"""Return only unread items in the current queryset"""
if is_soft_delete() and not include_deleted:
return self.filter(unread=True, deleted=False)
# When SOFT_DELETE=False, developers are supposed NOT to touch 'deleted' field.
# In this case, to improve query performance, don't filter by 'deleted' field
return self.filter(unread=True)
def read(self, include_deleted=False):
"""Return only read items in the current queryset"""
if is_soft_delete() and not include_deleted:
return self.filter(unread=False, deleted=False)
# When SOFT_DELETE=False, developers are supposed NOT to touch 'deleted' field.
# In this case, to improve query performance, don't filter by 'deleted' field
return self.filter(unread=False)
def mark_all_as_read(self, recipient=None):
"""Mark as read any unread messages in the current queryset.
Optionally, filter these by recipient first.
"""
# We want to filter out read ones, as later we will store
# the time they were marked as read.
qset = self.unread(True)
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(unread=False)
def mark_all_as_unread(self, recipient=None):
"""Mark as unread any read messages in the current queryset.
Optionally, filter these by recipient first.
"""
qset = self.read(True)
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(unread=True)
def deleted(self):
"""Return only deleted items in the current queryset"""
assert_soft_delete()
return self.filter(deleted=True)
def active(self):
"""Return only active(un-deleted) items in the current queryset"""
assert_soft_delete()
return self.filter(deleted=False)
def mark_all_as_deleted(self, recipient=None):
"""Mark current queryset as deleted.
Optionally, filter by recipient first.
"""
assert_soft_delete()
qset = self.active()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(deleted=True)
def mark_all_as_active(self, recipient=None):
"""Mark current queryset as active(un-deleted).
Optionally, filter by recipient first.
"""
assert_soft_delete()
qset = self.deleted()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(deleted=False)
def mark_as_unsent(self, recipient=None):
qset = self.sent()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(emailed=False)
def mark_as_sent(self, recipient=None):
qset = self.unsent()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(emailed=True)
class NotificationLevel(models.IntegerChoices):
SUCCESS = 1
INFO = 2
WARNING = 3
ERROR = 4
class AbstractNotification(models.Model):
"""
Action model describing the actor acting out a verb (on an optional
target).
Nomenclature based on http://activitystrea.ms/specs/atom/1.0/
Generalized Format::
<actor> <verb> <time>
<actor> <verb> <target> <time>
<actor> <verb> <action_object> <target> <time>
Examples::
<justquick> <reached level 60> <1 minute ago>
<brosner> <commented on> <pinax/pinax> <2 hours ago>
<washingtontimes> <started follow> <justquick> <8 minutes ago>
<mitsuhiko> <closed> <issue 70> on <mitsuhiko/flask> <about 2 hours ago>
Unicode Representation::
justquick reached level 60 1 minute ago
mitsuhiko closed issue 70 on mitsuhiko/flask 3 hours ago
HTML Representation::
<a href="http://test.com/">brosner</a> commented on <a href="http://gh.com/pinax/pinax">pinax/pinax</a> 2 hours ago
"""
level = models.IntegerField(_("level"), choices=NotificationLevel.choices, default=NotificationLevel.INFO)
recipient = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notifications",
verbose_name=_("recipient"),
blank=False,
)
unread = models.BooleanField(_("unread"), default=True, blank=False, db_index=True)
actor_content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, related_name="notify_actor", verbose_name=_("actor content type")
)
actor_object_id = models.CharField(_("actor object id"), max_length=255)
actor = GenericForeignKey("actor_content_type", "actor_object_id")
actor.short_description = _("actor")
verb = models.CharField(_("verb"), max_length=255)
description = models.TextField(_("description"), blank=True, null=True)
target_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="notify_target",
verbose_name=_("target content type"),
blank=True,
null=True,
)
target_object_id = models.CharField(_("target object id"), max_length=255, blank=True, null=True)
target = GenericForeignKey("target_content_type", "target_object_id")
target.short_description = _("target")
action_object_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="notify_action_object",
verbose_name=_("action object content type"),
blank=True,
null=True,
)
action_object_object_id = models.CharField(_("action object object id"), max_length=255, blank=True, null=True)
action_object = GenericForeignKey("action_object_content_type", "action_object_object_id")
action_object.short_description = _("action object")
timestamp = models.DateTimeField(_("timestamp"), default=timezone.now, db_index=True)
public = models.BooleanField(_("public"), default=True, db_index=True)
deleted = models.BooleanField(_("deleted"), default=False, db_index=True)
emailed = models.BooleanField(_("emailed"), default=False, db_index=True)
data = models.JSONField(_("data"), blank=True, null=True)
objects = NotificationQuerySet.as_manager()
class Meta:
abstract = True
ordering = ("-timestamp",)
# speed up notifications count query
index_together = ("recipient", "unread")
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
def __str__(self):
ctx = {
"actor": self.actor,
"verb": self.verb,
"action_object": self.action_object,
"target": self.target,
"timesince": self.timesince(),
}
if self.target:
if self.action_object:
return _("%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago") % ctx
return _("%(actor)s %(verb)s %(target)s %(timesince)s ago") % ctx
if self.action_object:
return _("%(actor)s %(verb)s %(action_object)s %(timesince)s ago") % ctx
return _("%(actor)s %(verb)s %(timesince)s ago") % ctx
def timesince(self, now=None):
"""
Shortcut for the ``django.utils.timesince.timesince`` function of the
current timestamp.
"""
return timesince.timesince(self.timestamp, now)
@property
def slug(self):
return self.id
def mark_as_read(self):
if self.unread:
self.unread = False
self.save()
def mark_as_unread(self):
if not self.unread:
self.unread = True
self.save()
def actor_object_url(self):
try:
url = reverse(
f"admin:{self.actor_content_type.app_label}_{self.actor_content_type.model}_change",
args=(self.actor_object_id,),
)
return format_html("<a href='{url}'>{id}</a>", url=url, id=self.actor_object_id)
except NoReverseMatch:
return self.actor_object_id
def action_object_url(self):
try:
url = reverse(
f"admin:{self.action_object_content_type.app_label}_{self.action_object_content_type.model}_change",
args=(self.action_object_id,),
)
return format_html("<a href='{url}'>{id}</a>", url=url, id=self.action_object_object_id)
except NoReverseMatch:
return self.action_object_object_id
def target_object_url(self):
try:
url = reverse(
f"admin:{self.target_content_type.app_label}_{self.target_content_type.model}_change",
args=(self.target_object_id,),
)
return format_html("<a href='{url}'>{id}</a>", url=url, id=self.target_object_id)
except NoReverseMatch:
return self.target_object_id
# TODO: move to notifications/signals.py
def notify_handler(verb, **kwargs):
"""
Handler function to create Notification instance upon action signal call.
"""
# Pull the options out of kwargs
kwargs.pop("signal", None)
recipient = kwargs.pop("recipient")
actor = kwargs.pop("sender")
optional_objs = [(kwargs.pop(opt, None), opt) for opt in ("target", "action_object")]
public = bool(kwargs.pop("public", True))
description = kwargs.pop("description", None)
timestamp = kwargs.pop("timestamp", timezone.now())
Notification = load_model("notifications", "Notification") # pylint: disable=invalid-name
level = kwargs.pop("level", NotificationLevel.INFO)
actor_for_concrete_model = kwargs.pop("actor_for_concrete_model", True)
# Check if User or Group
if isinstance(recipient, Group):
recipients = recipient.user_set.all()
elif isinstance(recipient, (QuerySet, list)):
recipients = recipient
else:
recipients = [recipient]
new_notifications = []
for recipient in recipients:
newnotify = Notification(
recipient=recipient,
actor_content_type=ContentType.objects.get_for_model(actor, for_concrete_model=actor_for_concrete_model),
actor_object_id=actor.pk,
verb=str(verb),
public=public,
description=description,
timestamp=timestamp,
level=level,
)
# Set optional objects
for obj, opt in optional_objs:
if obj is not None:
for_concrete_model = kwargs.pop(f"{opt}_for_concrete_model", True)
setattr(newnotify, f"{opt}_object_id", obj.pk)
setattr(
newnotify,
f"{opt}_content_type",
ContentType.objects.get_for_model(obj, for_concrete_model=for_concrete_model),
)
if kwargs and EXTRA_DATA:
# set kwargs as model column if available
for key in list(kwargs.keys()):
if hasattr(newnotify, key):
setattr(newnotify, key, kwargs.pop(key))
newnotify.data = kwargs
newnotify.save()
new_notifications.append(newnotify)
return new_notifications
# TODO: Move to notifications/apps.py::ready()
notify.connect(notify_handler, dispatch_uid="notifications.models.notification")

View file

@ -2,7 +2,7 @@
from django.db import migrations
from ..base.models import NotificationLevel
from notifications.models import NotificationLevel
def copy_level(apps, *args):

View file

@ -1,22 +0,0 @@
from django.contrib.humanize.templatetags.humanize import naturalday, naturaltime
from swapper import swappable_setting
from .base.models import AbstractNotification
class Notification(AbstractNotification):
class Meta(AbstractNotification.Meta):
abstract = False
swappable = swappable_setting("notifications", "Notification")
def naturalday(self):
"""
Shortcut for the ``humanize``.
Take a parameter humanize_type. This parameter control the which humanize method use.
Return ``today``, ``yesterday`` ,``now``, ``2 seconds ago``etc.
"""
return naturalday(self.timestamp)
def naturaltime(self):
return naturaltime(self.timestamp)

View file

@ -0,0 +1,7 @@
from .base import NotificationLevel
from .notification import Notification
__all__ = [
"Notification",
"NotificationLevel",
]

View file

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
# pylint: disable=too-many-lines
import datetime
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.humanize.templatetags.humanize import naturalday, naturaltime
from django.db import models
from django.urls import NoReverseMatch, reverse
from django.utils import timesince, timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from notifications.querysets import NotificationQuerySet
class NotificationLevel(models.IntegerChoices):
SUCCESS = 1
INFO = 2
WARNING = 3
ERROR = 4
class AbstractNotification(models.Model):
"""
Action model describing the actor acting out a verb (on an optional
target).
Nomenclature based on http://activitystrea.ms/specs/atom/1.0/
Generalized Format::
<actor> <verb> <time>
<actor> <verb> <target> <time>
<actor> <verb> <action_object> <target> <time>
Examples::
<justquick> <reached level 60> <1 minute ago>
<brosner> <commented on> <pinax/pinax> <2 hours ago>
<washingtontimes> <started follow> <justquick> <8 minutes ago>
<mitsuhiko> <closed> <issue 70> on <mitsuhiko/flask> <about 2 hours ago>
Unicode Representation::
justquick reached level 60 1 minute ago
mitsuhiko closed issue 70 on mitsuhiko/flask 3 hours ago
HTML Representation::
<a href="http://test.com/">brosner</a> commented on <a href="http://gh.com/pinax/pinax">pinax/pinax</a> 2 hours ago
"""
level = models.IntegerField(_("level"), choices=NotificationLevel.choices, default=NotificationLevel.INFO)
recipient = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notifications",
verbose_name=_("recipient"),
blank=False,
)
unread = models.BooleanField(_("unread"), default=True, blank=False, db_index=True)
actor_content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, related_name="notify_actor", verbose_name=_("actor content type")
)
actor_object_id = models.CharField(_("actor object id"), max_length=255)
actor = GenericForeignKey("actor_content_type", "actor_object_id")
actor.short_description = _("actor")
verb = models.CharField(_("verb"), max_length=255)
description = models.TextField(_("description"), blank=True, null=True)
target_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="notify_target",
verbose_name=_("target content type"),
blank=True,
null=True,
)
target_object_id = models.CharField(_("target object id"), max_length=255, blank=True, null=True)
target = GenericForeignKey("target_content_type", "target_object_id")
target.short_description = _("target")
action_object_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name="notify_action_object",
verbose_name=_("action object content type"),
blank=True,
null=True,
)
action_object_object_id = models.CharField(_("action object object id"), max_length=255, blank=True, null=True)
action_object = GenericForeignKey("action_object_content_type", "action_object_object_id")
action_object.short_description = _("action object")
timestamp = models.DateTimeField(_("timestamp"), default=timezone.now, db_index=True)
public = models.BooleanField(_("public"), default=True, db_index=True)
deleted = models.BooleanField(_("deleted"), default=False, db_index=True)
emailed = models.BooleanField(_("emailed"), default=False, db_index=True)
data = models.JSONField(_("data"), blank=True, null=True)
objects = NotificationQuerySet.as_manager()
class Meta:
abstract = True
ordering = ("-timestamp",)
# speed up notifications count query
index_together = ("recipient", "unread")
verbose_name = _("Notification")
verbose_name_plural = _("Notifications")
def __str__(self):
ctx = {
"actor": self.actor,
"verb": self.verb,
"action_object": self.action_object,
"target": self.target,
"timesince": self.timesince(),
}
if self.target:
if self.action_object:
return _("%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago") % ctx
return _("%(actor)s %(verb)s %(target)s %(timesince)s ago") % ctx
if self.action_object:
return _("%(actor)s %(verb)s %(action_object)s %(timesince)s ago") % ctx
return _("%(actor)s %(verb)s %(timesince)s ago") % ctx
def timesince(self, now: None | datetime.datetime = None) -> str:
"""
Shortcut for the ``django.utils.timesince.timesince`` function of the
current timestamp.
"""
return timesince.timesince(self.timestamp, now)
@property
def slug(self):
return self.id
def mark_as_read(self) -> None:
if self.unread:
self.unread = False
self.save()
def mark_as_unread(self) -> None:
if not self.unread:
self.unread = True
self.save()
def actor_object_url(self) -> str:
try:
url = reverse(
f"admin:{self.actor_content_type.app_label}_{self.actor_content_type.model}_change",
args=(self.actor_object_id,),
)
return format_html("<a href='{url}'>{id}</a>", url=url, id=self.actor_object_id)
except NoReverseMatch:
return self.actor_object_id
def action_object_url(self) -> str:
try:
url = reverse(
f"admin:{self.action_object_content_type.app_label}_{self.action_object_content_type.model}_change",
args=(self.action_object_id,),
)
return format_html("<a href='{url}'>{id}</a>", url=url, id=self.action_object_object_id)
except NoReverseMatch:
return self.action_object_object_id
def target_object_url(self) -> str:
try:
url = reverse(
f"admin:{self.target_content_type.app_label}_{self.target_content_type.model}_change",
args=(self.target_object_id,),
)
return format_html("<a href='{url}'>{id}</a>", url=url, id=self.target_object_id)
except NoReverseMatch:
return self.target_object_id
def naturalday(self):
"""
Shortcut for the ``humanize``.
Take a parameter humanize_type. This parameter control the which humanize method use.
Return ``today``, ``yesterday`` ,``now``, ``2 seconds ago``etc.
"""
return naturalday(self.timestamp)
def naturaltime(self):
return naturaltime(self.timestamp)

View file

@ -0,0 +1,9 @@
from swapper import swappable_setting
from notifications.models.base import AbstractNotification
class Notification(AbstractNotification):
class Meta(AbstractNotification.Meta):
abstract = False
swappable = swappable_setting("notifications", "Notification")

118
notifications/querysets.py Normal file
View file

@ -0,0 +1,118 @@
from typing import Optional, Type
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.translation import gettext_lazy as _
from notifications import settings as notifications_settings
from notifications.types import AbstractUser
def is_soft_delete() -> bool:
return bool(notifications_settings.get_config()["SOFT_DELETE"])
def assert_soft_delete() -> None:
if not is_soft_delete():
# msg = """To use 'deleted' field, please set 'SOFT_DELETE'=True in settings.
# Otherwise NotificationQuerySet.unread and NotificationQuerySet.read do NOT filter by 'deleted' field.
# """
msg = "REVERTME"
raise ImproperlyConfigured(msg)
class NotificationQuerySet(models.QuerySet):
"""Notification QuerySet"""
def unsent(self) -> models.QuerySet["AbstractNotification"]:
return self.filter(emailed=False)
def sent(self) -> models.QuerySet["AbstractNotification"]:
return self.filter(emailed=True)
def unread(self, include_deleted: Optional[bool] = False) -> models.QuerySet["AbstractNotification"]:
"""Return only unread items in the current queryset"""
if is_soft_delete() and not include_deleted:
return self.filter(unread=True, deleted=False)
# When SOFT_DELETE=False, developers are supposed NOT to touch 'deleted' field.
# In this case, to improve query performance, don't filter by 'deleted' field
return self.filter(unread=True)
def read(self, include_deleted: Optional[bool] = False) -> models.QuerySet["AbstractNotification"]:
"""Return only read items in the current queryset"""
if is_soft_delete() and not include_deleted:
return self.filter(unread=False, deleted=False)
# When SOFT_DELETE=False, developers are supposed NOT to touch 'deleted' field.
# In this case, to improve query performance, don't filter by 'deleted' field
return self.filter(unread=False)
def mark_all_as_read(self, recipient: None | Type[AbstractUser] = None) -> int:
"""Mark as read any unread messages in the current queryset.
Optionally, filter these by recipient first.
"""
# We want to filter out read ones, as later we will store
# the time they were marked as read.
qset = self.unread(True)
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(unread=False)
def mark_all_as_unread(self, recipient: None | Type[AbstractUser] = None) -> int:
"""Mark as unread any read messages in the current queryset.
Optionally, filter these by recipient first.
"""
qset = self.read(True)
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(unread=True)
def deleted(self) -> models.QuerySet["AbstractNotification"]:
"""Return only deleted items in the current queryset"""
assert_soft_delete()
return self.filter(deleted=True)
def active(self) -> models.QuerySet["AbstractNotification"]:
"""Return only active(un-deleted) items in the current queryset"""
assert_soft_delete()
return self.filter(deleted=False)
def mark_all_as_deleted(self, recipient: None | Type[AbstractUser] = None) -> int:
"""Mark current queryset as deleted.
Optionally, filter by recipient first.
"""
assert_soft_delete()
qset = self.active()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(deleted=True)
def mark_all_as_active(self, recipient: None | Type[AbstractUser] = None) -> int:
"""Mark current queryset as active(un-deleted).
Optionally, filter by recipient first.
"""
assert_soft_delete()
qset = self.deleted()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(deleted=False)
def mark_as_unsent(self, recipient: None | Type[AbstractUser] = None) -> int:
qset = self.sent()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(emailed=False)
def mark_as_sent(self, recipient: None | Type[AbstractUser] = None) -> int:
qset = self.unsent()
if recipient:
qset = qset.filter(recipient=recipient)
return qset.update(emailed=True)

View file

@ -11,7 +11,7 @@ CONFIG_DEFAULTS = {
}
def get_config():
def get_config() -> dict[str, int | bool]:
user_config = getattr(settings, "DJANGO_NOTIFICATIONS_CONFIG", {})
config = CONFIG_DEFAULTS.copy()

View file

@ -1,5 +1,79 @@
""" Django notifications signal file """
# -*- coding: utf-8 -*-
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.dispatch import Signal
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from swapper import load_model
from notifications import settings as notifications_settings
from notifications.models.base import NotificationLevel
EXTRA_DATA = notifications_settings.get_config()["USE_JSONFIELD"]
def notify_handler(verb, **kwargs):
"""
Handler function to create Notification instance upon action signal call.
"""
# Pull the options out of kwargs
kwargs.pop("signal", None)
recipient = kwargs.pop("recipient")
actor = kwargs.pop("sender")
optional_objs = [(kwargs.pop(opt, None), opt) for opt in ("target", "action_object")]
public = bool(kwargs.pop("public", True))
description = kwargs.pop("description", None)
timestamp = kwargs.pop("timestamp", timezone.now())
Notification = load_model("notifications", "Notification") # pylint: disable=invalid-name
level = kwargs.pop("level", NotificationLevel.INFO)
actor_for_concrete_model = kwargs.pop("actor_for_concrete_model", True)
# Check if User or Group
if isinstance(recipient, Group):
recipients = recipient.user_set.all()
elif isinstance(recipient, (models.QuerySet, list)):
recipients = recipient
else:
recipients = [recipient]
new_notifications = []
for recipient in recipients:
newnotify = Notification(
recipient=recipient,
actor_content_type=ContentType.objects.get_for_model(actor, for_concrete_model=actor_for_concrete_model),
actor_object_id=actor.pk,
verb=str(verb),
public=public,
description=description,
timestamp=timestamp,
level=level,
)
# Set optional objects
for obj, opt in optional_objs:
if obj is not None:
for_concrete_model = kwargs.pop(f"{opt}_for_concrete_model", True)
setattr(newnotify, f"{opt}_object_id", obj.pk)
setattr(
newnotify,
f"{opt}_content_type",
ContentType.objects.get_for_model(obj, for_concrete_model=for_concrete_model),
)
if kwargs and EXTRA_DATA:
# set kwargs as model column if available
for key in list(kwargs.keys()):
if hasattr(newnotify, key):
setattr(newnotify, key, kwargs.pop(key))
newnotify.data = kwargs
newnotify.save()
new_notifications.append(newnotify)
return new_notifications
notify = Signal()

View file

@ -1,7 +1,7 @@
import factory
from ...base.models import NotificationLevel
from ..factories import users as user_factory
from notifications.models.base import NotificationLevel
from notifications.tests.factories import users as user_factory
def test_main_migration0002(migrator):

View file

@ -21,8 +21,7 @@ from django.urls import reverse
from django.utils.timezone import localtime
from swapper import load_model
from notifications.base.models import notify_handler
from notifications.signals import notify
from notifications.signals import notify, notify_handler
Notification = load_model("notifications", "Notification")
User = get_user_model()

5
notifications/types.py Normal file
View file

@ -0,0 +1,5 @@
from typing import NewType
from django.contrib.auth.base_user import AbstractBaseUser
AbstractUser = NewType("AbstractUser", AbstractBaseUser)

View file

@ -118,7 +118,7 @@ ignore = [
"migrations",
]
jobs = 0
django-settings-module = "notifications.tests.settings_for_tests"
django-settings-module = "notifications.settings"
[tool.pylint.DESIGN]
max-locals = 20