diff --git a/README.rst b/README.rst index 32048b4..d1e3f4f 100644 --- a/README.rst +++ b/README.rst @@ -401,6 +401,27 @@ In this example the target object can be of type Foo or Bar and the appropriate Thanks to @DaWy +``AbstractNotification`` model +------------------------------ + +In case you need to customize the notification model in order to add field or +customised features that depend on your application, you can inherit and extend +the ``AbstractNotification`` model, example: + +.. code-block:: python + + from django.db import models + from notifications.base.models import AbstractNotification + + + class Notification(AbstractNotification): + # custom field example + category = models.ForeignKey('myapp.Category', + on_delete=models.CASCADE) + + class Meta(AbstractNotification.Meta): + abstract = False + Notes ===== diff --git a/notifications/base/__init__.py b/notifications/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notifications/base/models.py b/notifications/base/models.py new file mode 100644 index 0000000..3fae25d --- /dev/null +++ b/notifications/base/models.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-lines +from distutils.version import \ + StrictVersion # pylint: disable=no-name-in-module,import-error + +from django import get_version +from django.conf import settings +from django.contrib.auth.models import Group +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.utils import timezone +from django.utils.encoding import python_2_unicode_compatible +from django.utils.six import text_type +from jsonfield.fields import JSONField +from model_utils import Choices + +from notifications import settings as notifications_settings +from notifications.signals import notify +from notifications.utils import id2slug +from swapper import load_model + +if StrictVersion(get_version()) >= StrictVersion('1.8.0'): + from django.contrib.contenttypes.fields import GenericForeignKey # noqa +else: + from django.contrib.contenttypes.generic import GenericForeignKey # noqa + + +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) + + +@python_2_unicode_compatible +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:: + +