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'
def ready(self):
import extensions.signals # noqa: F401
import abuse.signals # noqa: F401
from actstream import registry
registry.register(self.get_model('AbuseReport'))

View File

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

View File

@ -1,4 +1,8 @@
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'
REPORTED_EXTENSION = 'reported extension'

View File

@ -2,6 +2,8 @@
from collections import defaultdict
import logging
from django.conf import settings
from django.core.mail import send_mail
from django.core.management.base import BaseCommand
from django.utils import timezone
@ -30,9 +32,16 @@ class Command(BaseCommand):
Notification.objects.bulk_update(batch, ['processed_by_mailer_at', 'sent'])
if len(to_send) > 0:
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')
def send_email(recipient, to_send):
pass
def send_batch_notification_email(recipient, notifications):
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 django.contrib.admin.utils import NestedObjects
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.templatetags.static import static
from constants.activity import Verb
from common.model_mixins import TrackChangesMixin
from files.utils import get_sha256_from_value
@ -142,11 +144,44 @@ class User(TrackChangesMixin, AbstractUser):
return self.groups.filter(name='moderators').exists()
def is_subscribed(self, notification: 'Notification') -> bool:
# TODO user profile settings to subscribe to groups of action verbs
return True
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)
action = models.ForeignKey(Action, null=False, on_delete=models.CASCADE)
processed_by_mailer_at = models.DateTimeField(default=None, null=True)
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}'