django-notifications/notifications/models.py

313 lines
10 KiB
Python
Raw Normal View History

2018-05-31 03:18:45 +00:00
''' Django notifications models file '''
# -*- coding: utf-8 -*-
# pylint: disable=too-many-lines
from distutils.version import StrictVersion # pylint: disable=no-name-in-module,import-error
2018-05-30 05:18:33 +00:00
from django import get_version
from django.conf import settings
2018-05-30 05:18:33 +00:00
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
2018-05-30 05:18:33 +00:00
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.query import QuerySet
from django.utils import timezone
2018-05-30 05:18:33 +00:00
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
2015-04-28 12:10:49 +00:00
if StrictVersion(get_version()) >= StrictVersion('1.8.0'):
2018-05-31 03:18:45 +00:00
from django.contrib.contenttypes.fields import GenericForeignKey # noqa
2015-04-28 12:10:49 +00:00
else:
2018-05-31 03:18:45 +00:00
from django.contrib.contenttypes.generic import GenericForeignKey # noqa
EXTRA_DATA = notifications_settings.get_config()['USE_JSONFIELD']
2015-04-28 12:10:49 +00:00
2016-01-01 02:59:51 +00:00
def is_soft_delete():
2018-05-30 05:18:33 +00:00
return notifications_settings.get_config()['SOFT_DELETE']
def assert_soft_delete():
if not is_soft_delete():
2018-05-30 05:18:33 +00:00
msg = """To use 'deleted' field, please set 'SOFT_DELETE'=True in settings.
Otherwise NotificationQuerySet.unread and NotificationQuerySet.read do NOT filter by 'deleted' field.
"""
raise ImproperlyConfigured(msg)
class NotificationQuerySet(models.query.QuerySet):
2018-05-31 03:18:45 +00:00
''' Notification QuerySet '''
2017-02-23 17:27:00 +00:00
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)
2018-05-31 03:18:45 +00:00
# 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)
2015-02-04 15:25:20 +00:00
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)
2018-05-31 03:18:45 +00:00
# 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)
2015-02-04 15:25:20 +00:00
def mark_all_as_read(self, recipient=None):
2012-10-24 00:10:28 +00:00
"""Mark as read any unread messages in the current queryset.
2015-02-04 15:25:20 +00:00
2012-10-24 00:10:28 +00:00
Optionally, filter these by recipient first.
"""
2015-02-04 15:25:20 +00:00
# We want to filter out read ones, as later we will store
2012-10-24 00:10:28 +00:00
# the time they were marked as read.
2018-05-31 03:18:45 +00:00
qset = self.unread(True)
if recipient:
2018-05-31 03:18:45 +00:00
qset = qset.filter(recipient=recipient)
2015-02-04 15:25:20 +00:00
2018-05-31 03:18:45 +00:00
return qset.update(unread=False)
2015-02-04 15:25:20 +00:00
def mark_all_as_unread(self, recipient=None):
2012-10-24 00:10:28 +00:00
"""Mark as unread any read messages in the current queryset.
2015-02-04 15:25:20 +00:00
2012-10-24 00:10:28 +00:00
Optionally, filter these by recipient first.
"""
2018-05-31 03:18:45 +00:00
qset = self.read(True)
2015-02-04 15:25:20 +00:00
if recipient:
2018-05-31 03:18:45 +00:00
qset = qset.filter(recipient=recipient)
2015-02-04 15:25:20 +00:00
2018-05-31 03:18:45 +00:00
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()
2018-05-31 03:18:45 +00:00
qset = self.active()
if recipient:
2018-05-31 03:18:45 +00:00
qset = qset.filter(recipient=recipient)
2018-05-31 03:18:45 +00:00
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()
2018-05-31 03:18:45 +00:00
qset = self.deleted()
if recipient:
2018-05-31 03:18:45 +00:00
qset = qset.filter(recipient=recipient)
2018-05-31 03:18:45 +00:00
return qset.update(deleted=False)
def mark_as_unsent(self, recipient=None):
2018-05-31 03:18:45 +00:00
qset = self.sent()
if recipient:
2018-05-31 03:18:45 +00:00
qset = qset.filter(recipient=recipient)
return qset.update(emailed=False)
2017-02-23 17:27:00 +00:00
def mark_as_sent(self, recipient=None):
2018-05-31 03:18:45 +00:00
qset = self.unsent()
if recipient:
2018-05-31 03:18:45 +00:00
qset = qset.filter(recipient=recipient)
return qset.update(emailed=True)
2017-02-23 17:27:00 +00:00
class Notification(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::
2018-05-31 03:18:45 +00:00
<a href="http://oebfare.com/">brosner</a> commented on <a href="http://github.com/pinax/pinax">pinax/pinax</a> 2 hours ago # noqa
2012-10-24 23:11:21 +00:00
"""
LEVELS = Choices('success', 'info', 'warning', 'error')
level = models.CharField(choices=LEVELS, default=LEVELS.info, max_length=20)
2015-02-04 15:25:20 +00:00
2018-05-31 03:18:45 +00:00
recipient = models.ForeignKey(
settings.AUTH_USER_MODEL,
blank=False,
related_name='notifications',
on_delete=models.CASCADE
)
2018-02-19 14:22:03 +00:00
unread = models.BooleanField(default=True, blank=False, db_index=True)
2017-12-07 00:38:29 +00:00
actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor', on_delete=models.CASCADE)
actor_object_id = models.CharField(max_length=255)
2015-04-28 12:10:49 +00:00
actor = GenericForeignKey('actor_content_type', 'actor_object_id')
verb = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
2018-05-31 03:18:45 +00:00
target_content_type = models.ForeignKey(
ContentType,
related_name='notify_target',
blank=True,
null=True,
on_delete=models.CASCADE
)
target_object_id = models.CharField(max_length=255, blank=True, null=True)
2016-01-01 02:59:51 +00:00
target = GenericForeignKey('target_content_type', 'target_object_id')
2016-01-01 02:59:51 +00:00
action_object_content_type = models.ForeignKey(ContentType, blank=True, null=True,
2017-12-07 00:38:29 +00:00
related_name='notify_action_object', on_delete=models.CASCADE)
2016-01-01 02:59:51 +00:00
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)
2018-02-19 14:22:03 +00:00
public = models.BooleanField(default=True, db_index=True)
deleted = models.BooleanField(default=False, db_index=True)
emailed = models.BooleanField(default=False, db_index=True)
2015-02-04 15:25:20 +00:00
data = JSONField(blank=True, null=True)
2015-12-07 03:17:37 +00:00
objects = NotificationQuerySet.as_manager()
class Meta:
2018-05-31 03:18:45 +00:00
ordering = ('-timestamp',)
app_label = 'notifications'
def __unicode__(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 u'%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago' % ctx
return u'%(actor)s %(verb)s %(target)s %(timesince)s ago' % ctx
if self.action_object:
return u'%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx
return u'%(actor)s %(verb)s %(timesince)s ago' % ctx
2015-12-11 13:32:20 +00:00
def __str__(self): # Adds support for Python 3
return self.__unicode__()
def timesince(self, now=None):
"""
Shortcut for the ``django.utils.timesince.timesince`` function of the
current timestamp.
"""
from django.utils.timesince import timesince as timesince_
return timesince_(self.timestamp, now)
@property
def slug(self):
return id2slug(self.id)
def mark_as_read(self):
if self.unread:
self.unread = False
self.save()
2014-04-03 13:55:38 +00:00
2014-04-03 13:50:06 +00:00
def mark_as_unread(self):
2014-04-03 13:55:38 +00:00
if not self.unread:
2014-04-03 13:50:06 +00:00
self.unread = True
self.save()
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')
2016-01-01 02:59:51 +00:00
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())
2016-01-01 02:59:51 +00:00
level = kwargs.pop('level', Notification.LEVELS.info)
# Check if User or Group
if isinstance(recipient, Group):
recipients = recipient.user_set.all()
2018-05-31 03:18:45 +00:00
elif isinstance(recipient, (QuerySet, list)):
recipients = recipient
else:
recipients = [recipient]
new_notifications = []
for recipient in recipients:
newnotify = Notification(
2016-01-01 02:59:51 +00:00
recipient=recipient,
actor_content_type=ContentType.objects.get_for_model(actor),
actor_object_id=actor.pk,
verb=text_type(verb),
public=public,
description=description,
timestamp=timestamp,
level=level,
)
# Set optional objects
2016-01-01 02:59:51 +00:00
for obj, opt in optional_objs:
if obj is not None:
setattr(newnotify, '%s_object_id' % opt, obj.pk)
setattr(newnotify, '%s_content_type' % opt,
ContentType.objects.get_for_model(obj))
2018-05-31 03:18:45 +00:00
if kwargs and EXTRA_DATA:
newnotify.data = kwargs
newnotify.save()
new_notifications.append(newnotify)
return new_notifications
# connect the signal
notify.connect(notify_handler, dispatch_uid='notifications.models.notification')