Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions kitsune/notifications/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,23 @@ Notifications have only a few properties:

Notifications also don't have any direct alerting properties, but they are used
by other systems to alert users in some way.

Realtime Notifications
======================

Realtime notifications are generally similar to the Notifications above in
function. They have different semantics however. Realtime notification
registrations are meant to be short lived, about one session. They also are not
intended to be a general notifications, and will not be shown to clients beside
the requesting client. Most importantly, they are unaffected by a user's follow
preferences.

A realtime registration links a particular object to a simple push end point.
When any Action (as above) is created, it checks for any matching Realtime
registrations and then sends a SimplePush message. Clients are then expected to
check what actions they are being notified about.

In the view clients check for actions in, much more information is provided
compared to when a user checks for notifications. This is because the action list
is intended to be used to populate a UI in realtime, as opposed to sending short
notifications to users.
80 changes: 79 additions & 1 deletion kitsune/notifications/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from django.db.models import Q

import django_filters
from actstream.models import Action
from rest_framework import serializers, viewsets, permissions, mixins, status
from rest_framework.decorators import action
from rest_framework.response import Response

from kitsune.notifications.models import PushNotificationRegistration, Notification
from kitsune.notifications.models import (
PushNotificationRegistration, Notification, RealtimeRegistration)
from kitsune.sumo.api import OnlyCreatorEdits, DateTimeUTCField, GenericRelatedField


Expand Down Expand Up @@ -131,3 +135,77 @@ class PushNotificationRegistrationViewSet(mixins.CreateModelMixin,
permissions.IsAuthenticated,
OnlyCreatorEdits,
]


class RealtimeRegistrationSerializer(serializers.ModelSerializer):
endpoint = serializers.CharField(write_only=True)
creator = serializers.SlugRelatedField(slug_field='username', required=False)
content_type = serializers.SlugRelatedField(slug_field='name')

class Meta:
model = RealtimeRegistration
fields = [
'id',
'creator',
'created',
'endpoint',
'content_type',
'object_id',
]

def validate_creator(self, attrs, source):
authed_user = getattr(self.context.get('request'), 'user')
creator = attrs.get('creator')

if creator is None:
attrs['creator'] = authed_user
elif creator != authed_user:
raise serializers.ValidationError(
"Can't register push notifications for another user.")

return attrs


class RealtimeActionSerializer(serializers.ModelSerializer):
action_object = GenericRelatedField(serializer_type='full')
actor = GenericRelatedField(serializer_type='full')
target = GenericRelatedField(serializer_type='full')
verb = serializers.CharField()
timestamp = DateTimeUTCField()

class Meta:
model = PushNotificationRegistration
fields = (
'action_object',
'actor',
'id',
'target',
'timestamp',
'verb',
)


class RealtimeRegistrationViewSet(mixins.CreateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet):
model = RealtimeRegistration
serializer_class = RealtimeRegistrationSerializer
permission_classes = [
permissions.IsAuthenticated,
OnlyCreatorEdits,
]

@action(methods=['GET'])
def updates(self, request, pk=None):
"""Get all the actions that correspond to this registration."""
reg = self.get_object()

query = Q(actor_content_type=reg.content_type, actor_object_id=reg.object_id)
query |= Q(target_content_type=reg.content_type, target_object_id=reg.object_id)
query |= Q(action_object_content_type=reg.content_type,
action_object_object_id=reg.object_id)

actions = Action.objects.filter(query)
serializer = RealtimeActionSerializer(actions, many=True)

return Response(serializer.data)
43 changes: 18 additions & 25 deletions kitsune/notifications/models.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import logging
from datetime import datetime

from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver

import actstream.registry
from actstream.models import Action
import requests
from requests.exceptions import RequestException

from kitsune.sumo.models import ModelBase
from kitsune.notifications.decorators import notification_handler


logger = logging.getLogger('k.notifications')


class Notification(ModelBase):
Expand Down Expand Up @@ -62,21 +57,19 @@ def send_notification(sender, instance, created, **kwargs):
tasks.send_notification.delay(instance.id)


@notification_handler
def simple_push(notification):
"""
Send simple push notifications to users that have opted in to them.

This will be called as a part of a celery task.
"""
registrations = PushNotificationRegistration.objects.filter(creator=notification.owner)
for reg in registrations:
try:
r = requests.put(reg.push_url, 'version={}'.format(notification.id))
# If something does wrong, the SimplePush server will give back
# json encoded error messages.
if r.status_code != 200:
logger.error('SimplePush error: %s %s', r.status_code, r.json())
except RequestException as e:
# This will go to Sentry.
logger.error('SimplePush PUT failed: %s', e)
class RealtimeRegistration(ModelBase):
creator = models.ForeignKey(User)
created = models.DateTimeField(default=datetime.now)
endpoint = models.CharField(max_length=256)

content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
target = generic.GenericForeignKey('content_type', 'object_id')


@receiver(post_save, sender=Action, dispatch_uid='action_send_realtimes')
def send_realtimes_for_action(sender, instance, created, **kwargs):
if not created:
return
from kitsune.notifications import tasks # avoid circular import
tasks.send_realtimes_for_action.delay(instance.id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding model 'RealtimeRegistration'
db.create_table(u'notifications_realtimeregistration', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('creator', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
('endpoint', self.gf('django.db.models.fields.CharField')(max_length=256)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
))
db.send_create_signal(u'notifications', ['RealtimeRegistration'])

def backwards(self, orm):
# Deleting model 'RealtimeRegistration'
db.delete_table(u'notifications_realtimeregistration')

models = {
u'actstream.action': {
'Meta': {'ordering': "('-timestamp',)", 'object_name': 'Action'},
'action_object_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'action_object'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}),
'action_object_object_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'actor_content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'actor'", 'to': u"orm['contenttypes.ContentType']"}),
'actor_object_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'data': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'target_content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'target'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}),
'target_object_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'verb': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'notifications.notification': {
'Meta': {'object_name': 'Notification'},
'action': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['actstream.Action']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'read_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
},
u'notifications.pushnotificationregistration': {
'Meta': {'object_name': 'PushNotificationRegistration'},
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'push_url': ('django.db.models.fields.CharField', [], {'max_length': '256'})
},
u'notifications.realtimeregistration': {
'Meta': {'object_name': 'RealtimeRegistration'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'endpoint': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
}
}

complete_apps = ['notifications']
Loading