Notification emails #80
@ -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'))
|
||||||
|
@ -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(
|
||||||
|
@ -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'
|
||||||
|
@ -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],
|
||||||
|
)
|
||||||
|
@ -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}'
|
||||||
|
Loading…
Reference in New Issue
Block a user