From d48c8215f5147bdfce9635461e3279b973ca0ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Kr=C3=B6nke?= Date: Fri, 26 Oct 2018 12:23:35 +0200 Subject: [PATCH 01/31] add db_index to default ordering field timestamp --- notifications/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notifications/models.py b/notifications/models.py index ec6420a..fd570f4 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -197,7 +197,7 @@ class Notification(models.Model): action_object_object_id = models.CharField(max_length=255, blank=True, null=True) action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id') - timestamp = models.DateTimeField(default=timezone.now) + timestamp = models.DateTimeField(default=timezone.now, db_index=True) public = models.BooleanField(default=True, db_index=True) deleted = models.BooleanField(default=False, db_index=True) From 3f3d947581dddcc7ab2c7c13299d41446ad191bd Mon Sep 17 00:00:00 2001 From: Tobias Kroenke Date: Fri, 26 Oct 2018 13:41:06 +0200 Subject: [PATCH 02/31] add generated migration --- .../migrations/0007_auto_20181026_0541.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 notifications/migrations/0007_auto_20181026_0541.py diff --git a/notifications/migrations/0007_auto_20181026_0541.py b/notifications/migrations/0007_auto_20181026_0541.py new file mode 100644 index 0000000..25d84e1 --- /dev/null +++ b/notifications/migrations/0007_auto_20181026_0541.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.9 on 2018-10-26 10:41 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0006_indexes'), + ] + + operations = [ + migrations.AlterField( + model_name='notification', + name='timestamp', + field=models.DateTimeField(db_index=True, default=django.utils.timezone.now), + ), + ] From 1d7e3dcd43a8a3181a1a4aaea404ec2be65c55a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Kr=C3=B6nke?= Date: Tue, 22 Jan 2019 09:59:35 +0100 Subject: [PATCH 03/31] Rename 0007_auto_20181026_0541.py to 0007_add_timestamp_index.py --- .../{0007_auto_20181026_0541.py => 0007_add_timestamp_index.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename notifications/migrations/{0007_auto_20181026_0541.py => 0007_add_timestamp_index.py} (100%) diff --git a/notifications/migrations/0007_auto_20181026_0541.py b/notifications/migrations/0007_add_timestamp_index.py similarity index 100% rename from notifications/migrations/0007_auto_20181026_0541.py rename to notifications/migrations/0007_add_timestamp_index.py From 01e9cb97e2dbeb74a6e3fd90fb922bebc22e1ffc Mon Sep 17 00:00:00 2001 From: Ehmad Zubair Date: Thu, 18 Apr 2019 14:58:42 +0500 Subject: [PATCH 04/31] fix: Compatibility with Django 2.1+ --- notifications/tests/settings.py | 2 ++ notifications/tests/urls.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/notifications/tests/settings.py b/notifications/tests/settings.py index bd13d81..c5e084b 100644 --- a/notifications/tests/settings.py +++ b/notifications/tests/settings.py @@ -20,6 +20,7 @@ DATABASES = { MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware' ) # Django >= 2.0 @@ -29,6 +30,7 @@ INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', + 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sessions', 'notifications.tests', diff --git a/notifications/tests/urls.py b/notifications/tests/urls.py index 60c745d..f1ad894 100644 --- a/notifications/tests/urls.py +++ b/notifications/tests/urls.py @@ -4,12 +4,22 @@ from distutils.version import StrictVersion # pylint: disable=no-name-in-module from django import get_version from django.contrib import admin -from django.contrib.auth.views import login from notifications.tests.views import (live_tester, # pylint: disable=no-name-in-module,import-error make_notification) -if StrictVersion(get_version()) >= StrictVersion('2.0'): +if StrictVersion(get_version()) >= StrictVersion('2.1'): from django.urls import include, path # noqa + from django.contrib.auth.views import LoginView + urlpatterns = [ + path('test_make/', make_notification), + path('test/', live_tester), + path('login/', LoginView.as_view(), name='login'), # reverse for django login is not working + path('admin/', admin.site.urls), + path('', include('notifications.urls', namespace='notifications')), + ] +elif StrictVersion(get_version()) >= StrictVersion('2.0') and StrictVersion(get_version()) < StrictVersion('2.1'): + from django.urls import include, path # noqa + from django.contrib.auth.views import login urlpatterns = [ path('test_make/', make_notification), path('test/', live_tester), @@ -19,6 +29,7 @@ if StrictVersion(get_version()) >= StrictVersion('2.0'): ] else: from django.conf.urls import include, url + from django.contrib.auth.views import login urlpatterns = [ url(r'^login/$', login, name='login'), # reverse for django login is not working url(r'^test_make/', make_notification), From d55aae4e3d69064d0aead3bb04d6193016a7f24f Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Sun, 28 Apr 2019 14:59:42 -0400 Subject: [PATCH 05/31] [models] Added AbstractNotification #202 Implements and closes #102 --- README.rst | 21 ++ notifications/base/__init__.py | 0 notifications/base/models.py | 317 ++++++++++++++++++ notifications/migrations/0001_initial.py | 2 + .../0008_index_together_recipient_unread.py | 19 ++ notifications/models.py | 314 +---------------- setup.py | 3 +- 7 files changed, 367 insertions(+), 309 deletions(-) create mode 100644 notifications/base/__init__.py create mode 100644 notifications/base/models.py create mode 100644 notifications/migrations/0008_index_together_recipient_unread.py 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:: + +