forked from mirrors/bookwyrm
Merge pull request #609 from mouse-reeve/model-notifications
Refactors generating notifications
This commit is contained in:
commit
5e2555dc0e
13 changed files with 132 additions and 121 deletions
|
@ -4,7 +4,6 @@ import logging
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.models import ImportJob, ImportItem
|
from bookwyrm.models import ImportJob, ImportItem
|
||||||
from bookwyrm.status import create_notification
|
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -68,7 +67,6 @@ def import_data(job_id):
|
||||||
item.fail_reason = 'Could not find a match for book'
|
item.fail_reason = 'Could not find a match for book'
|
||||||
item.save()
|
item.save()
|
||||||
finally:
|
finally:
|
||||||
create_notification(job.user, 'IMPORT', related_import=job)
|
|
||||||
job.complete = True
|
job.complete = True
|
||||||
job.save()
|
job.save()
|
||||||
|
|
||||||
|
|
|
@ -136,14 +136,7 @@ def handle_follow(activity):
|
||||||
)
|
)
|
||||||
# send the accept normally for a duplicate request
|
# send the accept normally for a duplicate request
|
||||||
|
|
||||||
manually_approves = relationship.user_object.manually_approves_followers
|
if not relationship.user_object.manually_approves_followers:
|
||||||
|
|
||||||
status_builder.create_notification(
|
|
||||||
relationship.user_object,
|
|
||||||
'FOLLOW_REQUEST' if manually_approves else 'FOLLOW',
|
|
||||||
related_user=relationship.user_subject
|
|
||||||
)
|
|
||||||
if not manually_approves:
|
|
||||||
relationship.accept()
|
relationship.accept()
|
||||||
|
|
||||||
|
|
||||||
|
@ -256,27 +249,6 @@ def handle_create_status(activity):
|
||||||
# it was discarded because it's not a bookwyrm type
|
# it was discarded because it's not a bookwyrm type
|
||||||
return
|
return
|
||||||
|
|
||||||
# create a notification if this is a reply
|
|
||||||
notified = []
|
|
||||||
if status.reply_parent and status.reply_parent.user.local:
|
|
||||||
notified.append(status.reply_parent.user)
|
|
||||||
status_builder.create_notification(
|
|
||||||
status.reply_parent.user,
|
|
||||||
'REPLY',
|
|
||||||
related_user=status.user,
|
|
||||||
related_status=status,
|
|
||||||
)
|
|
||||||
if status.mention_users.exists():
|
|
||||||
for mentioned_user in status.mention_users.all():
|
|
||||||
if not mentioned_user.local or mentioned_user in notified:
|
|
||||||
continue
|
|
||||||
status_builder.create_notification(
|
|
||||||
mentioned_user,
|
|
||||||
'MENTION',
|
|
||||||
related_user=status.user,
|
|
||||||
related_status=status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def handle_delete_status(activity):
|
def handle_delete_status(activity):
|
||||||
|
@ -309,13 +281,6 @@ def handle_favorite(activity):
|
||||||
if fav.user.local:
|
if fav.user.local:
|
||||||
return
|
return
|
||||||
|
|
||||||
status_builder.create_notification(
|
|
||||||
fav.status.user,
|
|
||||||
'FAVORITE',
|
|
||||||
related_user=fav.user,
|
|
||||||
related_status=fav.status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def handle_unfavorite(activity):
|
def handle_unfavorite(activity):
|
||||||
|
@ -332,19 +297,11 @@ def handle_unfavorite(activity):
|
||||||
def handle_boost(activity):
|
def handle_boost(activity):
|
||||||
''' someone gave us a boost! '''
|
''' someone gave us a boost! '''
|
||||||
try:
|
try:
|
||||||
boost = activitypub.Boost(**activity).to_model(models.Boost)
|
activitypub.Boost(**activity).to_model(models.Boost)
|
||||||
except activitypub.ActivitySerializerError:
|
except activitypub.ActivitySerializerError:
|
||||||
# this probably just means we tried to boost an unknown status
|
# this probably just means we tried to boost an unknown status
|
||||||
return
|
return
|
||||||
|
|
||||||
if not boost.user.local:
|
|
||||||
status_builder.create_notification(
|
|
||||||
boost.boosted_status.user,
|
|
||||||
'BOOST',
|
|
||||||
related_user=boost.user,
|
|
||||||
related_status=boost.boosted_status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def handle_unboost(activity):
|
def handle_unboost(activity):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
''' like/fav/star a status '''
|
''' like/fav/star a status '''
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -22,6 +23,30 @@ class Favorite(ActivityMixin, BookWyrmModel):
|
||||||
self.user.save(broadcast=False)
|
self.user.save(broadcast=False)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.status.user.local and self.status.user != self.user:
|
||||||
|
notification_model = apps.get_model(
|
||||||
|
'bookwyrm.Notification', require_ready=True)
|
||||||
|
notification_model.objects.create(
|
||||||
|
user=self.status.user,
|
||||||
|
notification_type='FAVORITE',
|
||||||
|
related_user=self.user,
|
||||||
|
related_status=self.status
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
''' delete and delete notifications '''
|
||||||
|
# check for notification
|
||||||
|
if self.status.user.local:
|
||||||
|
notification_model = apps.get_model(
|
||||||
|
'bookwyrm.Notification', require_ready=True)
|
||||||
|
notification = notification_model.objects.filter(
|
||||||
|
user=self.status.user, related_user=self.user,
|
||||||
|
related_status=self.status, notification_type='FAVORITE'
|
||||||
|
).first()
|
||||||
|
if notification:
|
||||||
|
notification.delete()
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' can't fav things twice '''
|
''' can't fav things twice '''
|
||||||
unique_together = ('user', 'status')
|
unique_together = ('user', 'status')
|
||||||
|
|
|
@ -263,6 +263,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||||
if formatted is None or formatted is MISSING:
|
if formatted is None or formatted is MISSING:
|
||||||
return
|
return
|
||||||
getattr(instance, self.name).set(formatted)
|
getattr(instance, self.name).set(formatted)
|
||||||
|
instance.save(broadcast=False)
|
||||||
|
|
||||||
def field_to_activity(self, value):
|
def field_to_activity(self, value):
|
||||||
if self.link_only:
|
if self.link_only:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import re
|
import re
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -50,6 +51,18 @@ class ImportJob(models.Model):
|
||||||
)
|
)
|
||||||
retry = models.BooleanField(default=False)
|
retry = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
''' save and notify '''
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if self.complete:
|
||||||
|
notification_model = apps.get_model(
|
||||||
|
'bookwyrm.Notification', require_ready=True)
|
||||||
|
notification_model.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
notification_type='IMPORT',
|
||||||
|
related_import=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ImportItem(models.Model):
|
class ImportItem(models.Model):
|
||||||
''' a single line of a csv being imported '''
|
''' a single line of a csv being imported '''
|
||||||
|
|
|
@ -25,6 +25,21 @@ class Notification(BookWyrmModel):
|
||||||
notification_type = models.CharField(
|
notification_type = models.CharField(
|
||||||
max_length=255, choices=NotificationType.choices)
|
max_length=255, choices=NotificationType.choices)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
''' save, but don't make dupes '''
|
||||||
|
# there's probably a better way to do this
|
||||||
|
if self.__class__.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
related_book=self.related_book,
|
||||||
|
related_user=self.related_user,
|
||||||
|
related_status=self.related_status,
|
||||||
|
related_import=self.related_import,
|
||||||
|
related_list_item=self.related_list_item,
|
||||||
|
notification_type=self.notification_type,
|
||||||
|
).exists():
|
||||||
|
return
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
''' checks if notifcation is in enum list for valid types '''
|
''' checks if notifcation is in enum list for valid types '''
|
||||||
constraints = [
|
constraints = [
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
''' defines relationships between users '''
|
''' defines relationships between users '''
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
@ -90,9 +91,20 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||||
return None
|
return None
|
||||||
except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist):
|
except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if broadcast and self.user_subject.local and not self.user_object.local:
|
if broadcast and self.user_subject.local and not self.user_object.local:
|
||||||
self.broadcast(self.to_activity(), self.user_subject)
|
self.broadcast(self.to_activity(), self.user_subject)
|
||||||
|
|
||||||
|
if self.user_object.local:
|
||||||
|
model = apps.get_model('bookwyrm.Notification', require_ready=True)
|
||||||
|
notification_type = 'FOLLOW_REQUEST' \
|
||||||
|
if self.user_object.manually_approves_followers else 'FOLLOW'
|
||||||
|
model.objects.create(
|
||||||
|
user=self.user_object,
|
||||||
|
related_user=self.user_subject,
|
||||||
|
notification_type=notification_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
''' turn this request into the real deal'''
|
''' turn this request into the real deal'''
|
||||||
|
|
|
@ -52,6 +52,38 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
serialize_reverse_fields = [('attachments', 'attachment', 'id')]
|
serialize_reverse_fields = [('attachments', 'attachment', 'id')]
|
||||||
deserialize_reverse_fields = [('attachments', 'attachment')]
|
deserialize_reverse_fields = [('attachments', 'attachment')]
|
||||||
|
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
''' save and notify '''
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
notification_model = apps.get_model(
|
||||||
|
'bookwyrm.Notification', require_ready=True)
|
||||||
|
|
||||||
|
if self.deleted:
|
||||||
|
notification_model.objects.filter(related_status=self).delete()
|
||||||
|
|
||||||
|
if self.reply_parent and self.reply_parent.user != self.user and \
|
||||||
|
self.reply_parent.user.local:
|
||||||
|
notification_model.objects.create(
|
||||||
|
user=self.reply_parent.user,
|
||||||
|
notification_type='REPLY',
|
||||||
|
related_user=self.user,
|
||||||
|
related_status=self,
|
||||||
|
)
|
||||||
|
for mention_user in self.mention_users.all():
|
||||||
|
# avoid double-notifying about this status
|
||||||
|
if not mention_user.local or \
|
||||||
|
(self.reply_parent and \
|
||||||
|
mention_user == self.reply_parent.user):
|
||||||
|
continue
|
||||||
|
notification_model.objects.create(
|
||||||
|
user=mention_user,
|
||||||
|
notification_type='MENTION',
|
||||||
|
related_user=self.user,
|
||||||
|
related_status=self,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def recipients(self):
|
def recipients(self):
|
||||||
''' tagged users who definitely need to get this status in broadcast '''
|
''' tagged users who definitely need to get this status in broadcast '''
|
||||||
|
@ -236,6 +268,33 @@ class Boost(ActivityMixin, Status):
|
||||||
)
|
)
|
||||||
activity_serializer = activitypub.Boost
|
activity_serializer = activitypub.Boost
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
''' save and notify '''
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
if not self.boosted_status.user.local:
|
||||||
|
return
|
||||||
|
|
||||||
|
notification_model = apps.get_model(
|
||||||
|
'bookwyrm.Notification', require_ready=True)
|
||||||
|
notification_model.objects.create(
|
||||||
|
user=self.boosted_status.user,
|
||||||
|
related_status=self.boosted_status,
|
||||||
|
related_user=self.user,
|
||||||
|
notification_type='BOOST',
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
''' delete and un-notify '''
|
||||||
|
notification_model = apps.get_model(
|
||||||
|
'bookwyrm.Notification', require_ready=True)
|
||||||
|
notification_model.objects.filter(
|
||||||
|
user=self.boosted_status.user,
|
||||||
|
related_status=self.boosted_status,
|
||||||
|
related_user=self.user,
|
||||||
|
notification_type='BOOST',
|
||||||
|
).delete()
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
''' the user field is "actor" here instead of "attributedTo" '''
|
''' the user field is "actor" here instead of "attributedTo" '''
|
||||||
|
|
|
@ -35,19 +35,3 @@ def create_generated_note(user, content, mention_books=None, privacy='public'):
|
||||||
status.mention_books.set(mention_books)
|
status.mention_books.set(mention_books)
|
||||||
status.save(created=True)
|
status.save(created=True)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
def create_notification(user, notification_type, related_user=None, \
|
|
||||||
related_book=None, related_status=None, related_import=None):
|
|
||||||
''' let a user know when someone interacts with their content '''
|
|
||||||
if user == related_user:
|
|
||||||
# don't create notification when you interact with your own stuff
|
|
||||||
return
|
|
||||||
models.Notification.objects.create(
|
|
||||||
user=user,
|
|
||||||
related_book=related_book,
|
|
||||||
related_user=related_user,
|
|
||||||
related_status=related_status,
|
|
||||||
related_import=related_import,
|
|
||||||
notification_type=notification_type,
|
|
||||||
)
|
|
||||||
|
|
|
@ -65,9 +65,9 @@ class TemplateTags(TestCase):
|
||||||
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
|
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
|
||||||
|
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=self.user, notification_type='FOLLOW')
|
user=self.user, notification_type='FAVORITE')
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=self.user, notification_type='FOLLOW')
|
user=self.user, notification_type='MENTION')
|
||||||
|
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=self.remote_user, notification_type='FOLLOW')
|
user=self.remote_user, notification_type='FOLLOW')
|
||||||
|
|
|
@ -30,7 +30,7 @@ class NotificationViews(TestCase):
|
||||||
def test_clear_notifications(self):
|
def test_clear_notifications(self):
|
||||||
''' erase notifications '''
|
''' erase notifications '''
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=self.local_user, notification_type='MENTION')
|
user=self.local_user, notification_type='FAVORITE')
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=self.local_user, notification_type='MENTION', read=True)
|
user=self.local_user, notification_type='MENTION', read=True)
|
||||||
self.assertEqual(models.Notification.objects.count(), 2)
|
self.assertEqual(models.Notification.objects.count(), 2)
|
||||||
|
|
|
@ -7,7 +7,6 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.status import create_notification
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable= no-self-use
|
# pylint: disable= no-self-use
|
||||||
|
@ -26,13 +25,6 @@ class Favorite(View):
|
||||||
# you already fav'ed that
|
# you already fav'ed that
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
if status.user.local:
|
|
||||||
create_notification(
|
|
||||||
status.user,
|
|
||||||
'FAVORITE',
|
|
||||||
related_user=request.user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,15 +44,6 @@ class Unfavorite(View):
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
||||||
favorite.delete()
|
favorite.delete()
|
||||||
|
|
||||||
# check for notification
|
|
||||||
if status.user.local:
|
|
||||||
notification = models.Notification.objects.filter(
|
|
||||||
user=status.user, related_user=request.user,
|
|
||||||
related_status=status, notification_type='FAVORITE'
|
|
||||||
).first()
|
|
||||||
if notification:
|
|
||||||
notification.delete()
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,14 +67,6 @@ class Boost(View):
|
||||||
privacy=status.privacy,
|
privacy=status.privacy,
|
||||||
user=request.user,
|
user=request.user,
|
||||||
)
|
)
|
||||||
|
|
||||||
if status.user.local:
|
|
||||||
create_notification(
|
|
||||||
status.user,
|
|
||||||
'BOOST',
|
|
||||||
related_user=request.user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,13 +81,4 @@ class Unboost(View):
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
boost.delete()
|
boost.delete()
|
||||||
|
|
||||||
# delete related notification
|
|
||||||
if status.user.local:
|
|
||||||
notification = models.Notification.objects.filter(
|
|
||||||
user=status.user, related_user=request.user,
|
|
||||||
related_status=status, notification_type='BOOST'
|
|
||||||
).first()
|
|
||||||
if notification:
|
|
||||||
notification.delete()
|
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
|
@ -10,7 +10,7 @@ from markdown import markdown
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.sanitize_html import InputHtmlParser
|
from bookwyrm.sanitize_html import InputHtmlParser
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from bookwyrm.status import create_notification, delete_status
|
from bookwyrm.status import delete_status
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
from .helpers import handle_remote_webfinger
|
from .helpers import handle_remote_webfinger
|
||||||
|
|
||||||
|
@ -48,31 +48,12 @@ class CreateStatus(View):
|
||||||
r'<a href="%s">%s</a>\g<1>' % \
|
r'<a href="%s">%s</a>\g<1>' % \
|
||||||
(mention_user.remote_id, mention_text),
|
(mention_user.remote_id, mention_text),
|
||||||
content)
|
content)
|
||||||
# add reply parent to mentions and notify
|
# add reply parent to mentions
|
||||||
if status.reply_parent:
|
if status.reply_parent:
|
||||||
status.mention_users.add(status.reply_parent.user)
|
status.mention_users.add(status.reply_parent.user)
|
||||||
|
|
||||||
if status.reply_parent.user.local:
|
|
||||||
create_notification(
|
|
||||||
status.reply_parent.user,
|
|
||||||
'REPLY',
|
|
||||||
related_user=request.user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
# deduplicate mentions
|
# deduplicate mentions
|
||||||
status.mention_users.set(set(status.mention_users.all()))
|
status.mention_users.set(set(status.mention_users.all()))
|
||||||
# create mention notifications
|
|
||||||
for mention_user in status.mention_users.all():
|
|
||||||
if status.reply_parent and mention_user == status.reply_parent.user:
|
|
||||||
continue
|
|
||||||
if mention_user.local:
|
|
||||||
create_notification(
|
|
||||||
mention_user,
|
|
||||||
'MENTION',
|
|
||||||
related_user=request.user,
|
|
||||||
related_status=status
|
|
||||||
)
|
|
||||||
|
|
||||||
# don't apply formatting to generated notes
|
# don't apply formatting to generated notes
|
||||||
if not isinstance(status, models.GeneratedNote):
|
if not isinstance(status, models.GeneratedNote):
|
||||||
|
|
Loading…
Reference in a new issue