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
11 changes: 11 additions & 0 deletions kitsune/notifications/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
notification_handlers = set()


def notification_handler(fn):
"""
Register a function to be called via Celery for every notification.

This may be used as a decorator or as a simple function.
"""
notification_handlers.add(fn)
return fn
37 changes: 36 additions & 1 deletion kitsune/notifications/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from datetime import datetime

from django.contrib.auth.models import User
Expand All @@ -7,8 +8,14 @@

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 @@ -41,7 +48,35 @@ class PushNotificationRegistration(ModelBase):
@receiver(post_save, sender=Action, dispatch_uid='action_create_notifications')
def add_notification_for_action(sender, instance, created, **kwargs):
"""When an Action is created, notify every user following something in the Action."""
from kitsune.notifications import tasks # avoid circular import
if not created:
return
from kitsune.notifications import tasks # avoid circular import
tasks.add_notification_for_action.delay(instance.id)


@receiver(post_save, sender=Notification, dispatch_uid='send_notification')
def send_notification(sender, instance, created, **kwargs):
if not created:
return
from kitsune.notifications import tasks # avoid circular import
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)
13 changes: 12 additions & 1 deletion kitsune/notifications/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from celery import task

from kitsune.notifications.models import Notification
from kitsune.notifications.decorators import notification_handlers


@task()
@task(ignore_result=True)
def add_notification_for_action(action_id):
action = Action.objects.get(id=action_id)

Expand All @@ -33,9 +34,19 @@ def add_notification_for_action(action_id):
object_id=action.action_object.pk,
actor_only=False)

query = query & ~Q(user=action.actor)

# execute the above query, iterate through the results, get every user
# assocated with those Follow objects, and fire off a notification to
# every one of them. Use a set to only notify each user once.
users_to_notify = set(f.user for f in Follow.objects.filter(query))
notifications = [Notification(owner=u, action=action) for u in users_to_notify]
Notification.objects.bulk_create(notifications)


@task(ignore_result=True)
def send_notification(notification_id):
"""Call every notification handler for a notification."""
notification = Notification.objects.get(id=notification_id)
for handler in notification_handlers:
handler(notification)
38 changes: 35 additions & 3 deletions kitsune/notifications/tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from nose.tools import eq_

from actstream.actions import follow
from actstream.signals import action
from actstream.models import Action, Follow
from mock import patch
from nose.tools import eq_

from kitsune.notifications.models import Notification
from kitsune.notifications import models as notification_models
from kitsune.notifications.models import Notification, PushNotificationRegistration
from kitsune.notifications.tests import notification
from kitsune.questions.tests import answer, question
from kitsune.sumo.tests import TestCase
from kitsune.users.tests import profile
Expand Down Expand Up @@ -58,3 +60,33 @@ def test_following_action_object(self):

eq_(notification.owner, follower)
eq_(notification.action, act)

def test_no_action_for_self(self):
"""Test that a notification is not sent for actions the user took."""
follower = profile().user
q = question(creator=follower, save=True)
# The above might make follows, which this test isn't about. Clear them out.
Follow.objects.all().delete()
follow(follower, q, actor_only=False)

# Make a new action for the above. This should not trigger notifications.
action.send(q.creator, verb='edited', action_object=q)
act = Action.objects.order_by('-id')[0]
eq_(Notification.objects.filter(action=act).count(), 0)


@patch.object(notification_models, 'requests')
class TestSimplePushNotifier(TestCase):

def test_simple_push_send(self, requests):
"""Verify that SimplePush registrations are called."""
u = profile().user
url = 'http://example.com/simple_push/asdf'
PushNotificationRegistration.objects.create(creator=u, push_url=url)
n = notification(owner=u, save=True)
requests.put.assert_called_once_with(url, 'version={}'.format(n.id))

def test_simple_push_not_sent(self, requests):
"""Verify that no request is made when there is no SimplePush registration."""
notification(save=True)
requests.put.assert_not_called()
7 changes: 4 additions & 3 deletions kitsune/questions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def save(self, update=False, *args, **kwargs):

# actstream
# Authors should automatically follow their own questions.
actstream.actions.follow(self.creator, self, actor_only=False)
actstream.actions.follow(self.creator, self, send_action=False, actor_only=False)

def add_metadata(self, **kwargs):
"""Add (save to db) the passed in metadata.
Expand Down Expand Up @@ -974,8 +974,9 @@ def save(self, update=True, no_notify=False, *args, **kwargs):
QuestionReplyEvent(self).fire(exclude=self.creator)

# actstream
actstream.actions.follow(self.creator, self, actor_only=False)
actstream.actions.follow(self.creator, self.question, actor_only=False)
actstream.actions.follow(self.creator, self, send_action=False, actor_only=False)
actstream.actions.follow(
self.creator, self.question, send_action=False, actor_only=False)

if not new:
# Clear the attached images cache.
Expand Down