Notification emails #80

Merged
Oleg-Komarov merged 31 commits from notifications into main 2024-04-18 16:11:20 +02:00
5 changed files with 57 additions and 6 deletions
Showing only changes of commit e4a77c6a9f - Show all commits

View File

@ -6,7 +6,7 @@ class AbuseConfig(AppConfig):
name = 'abuse' name = 'abuse'
def ready(self): def ready(self):
import extensions.signals # noqa: F401 import abuse.signals # noqa: F401
from actstream import registry from actstream import registry
registry.register(self.get_model('AbuseReport')) registry.register(self.get_model('AbuseReport'))

View File

@ -20,10 +20,13 @@ def create_action_from_report(
sender: object, sender: object,
instance: AbuseReport, instance: AbuseReport,
created: bool, created: bool,
raw: bool,
**kwargs: object, **kwargs: object,
) -> None: ) -> None:
if not created: if not created:
return return
if raw:
return
if instance.type == ABUSE_TYPE_EXTENSION: if instance.type == ABUSE_TYPE_EXTENSION:
verb = Verb.REPORTED_EXTENSION verb = Verb.REPORTED_EXTENSION
@ -31,9 +34,9 @@ def create_action_from_report(
verb = Verb.REPORTED_REVIEW verb = Verb.REPORTED_REVIEW
elif instance.type == ABUSE_TYPE_USER: elif instance.type == ABUSE_TYPE_USER:
# TODO? # TODO?
pass return
else: else:
logger.warning("ignoring an unexpected AbuseReport type={instance.type}") logger.warning(f'ignoring an unexpected AbuseReport type={instance.type}')
return return
action.send( action.send(

View File

@ -1,4 +1,8 @@
class Verb: class Verb:
"""These constants are used to dispatch Action records,
changing the values will result in a mismatch with historical values stored in db.
"""
SUBMITTED_FOR_REVIEW = 'submitted for review' SUBMITTED_FOR_REVIEW = 'submitted for review'
REPORTED_EXTENSION = 'reported extension' REPORTED_EXTENSION = 'reported extension'

View File

@ -2,6 +2,8 @@
from collections import defaultdict from collections import defaultdict
import logging import logging
from django.conf import settings
from django.core.mail import send_mail
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
@ -30,9 +32,16 @@ class Command(BaseCommand):
Notification.objects.bulk_update(batch, ['processed_by_mailer_at', 'sent']) Notification.objects.bulk_update(batch, ['processed_by_mailer_at', 'sent'])
if len(to_send) > 0: if len(to_send) > 0:
logger.info(f'sending an email to {recipient} about {len(to_send)} notifications') logger.info(f'sending an email to {recipient} about {len(to_send)} notifications')
send_email(recipient, to_send) send_batch_notification_email(recipient, to_send)
logger.info('finish') logger.info('finish')
def send_email(recipient, to_send): def send_batch_notification_email(recipient, notifications):
pass subject = 'New activity'
message = '\n\n'.join([n.format_email_txt() for n in notifications])
send_mail(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
[recipient.email],
)

View File

@ -6,9 +6,11 @@ import time
from actstream.models import Action from actstream.models import Action
from django.contrib.admin.utils import NestedObjects from django.contrib.admin.utils import NestedObjects
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.sites.models import Site
from django.db import models, DEFAULT_DB_ALIAS, transaction from django.db import models, DEFAULT_DB_ALIAS, transaction
from django.templatetags.static import static from django.templatetags.static import static
from constants.activity import Verb
from common.model_mixins import TrackChangesMixin from common.model_mixins import TrackChangesMixin
from files.utils import get_sha256_from_value from files.utils import get_sha256_from_value
@ -142,11 +144,44 @@ class User(TrackChangesMixin, AbstractUser):
return self.groups.filter(name='moderators').exists() return self.groups.filter(name='moderators').exists()
def is_subscribed(self, notification: 'Notification') -> bool: def is_subscribed(self, notification: 'Notification') -> bool:
# TODO user profile settings to subscribe to groups of action verbs
return True return True
class Notification(models.Model): class Notification(models.Model):
"""Notification records are created in Action's post_save signal.
When a user marks a notification as read, a record is deleted.
While a notification exists it can be picked up by a send_notifications management command
that runs periodically in background.
If a recipient opted out from receiving notifications of particular type (based on action.verb),
we shouldn't include it in the email, leaving sent=False.
"""
recipient = models.ForeignKey(User, null=False, on_delete=models.CASCADE) recipient = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
action = models.ForeignKey(Action, null=False, on_delete=models.CASCADE) action = models.ForeignKey(Action, null=False, on_delete=models.CASCADE)
processed_by_mailer_at = models.DateTimeField(default=None, null=True) processed_by_mailer_at = models.DateTimeField(default=None, null=True)
sent = models.BooleanField(default=False, null=False) sent = models.BooleanField(default=False, null=False)
def format_email_txt(self):
url = self.get_absolute_url()
# TODO construct a proper phrase, depending on the verb, maybe use a template
return f'{self.action.actor.full_name} {self.action.verb} {self.action.target}: {url}'
def get_absolute_url(self):
if self.action.verb == Verb.RATED_EXTENSION:
url = self.action.target.get_ratings_url()
elif self.action.verb in [
Verb.APPROVED,
Verb.COMMENTED,
Verb.REQUESTED_CHANGES,
Verb.REQUESTED_REVIEW,
]:
url = self.action.target.get_review_url()
elif self.action.action_object is not None:
url = self.action.action_object.get_absolute_url()
else:
url = self.action.target.get_absolute_url()
# TODO? url cloacking to auto-delete visited notifications
domain = Site.objects.get_current().domain
return f'https://{domain}{url}'