Notification emails #80
38
users/management/commands/send_notifications.py
Normal file
38
users/management/commands/send_notifications.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Send user notifications as emails, at most once delivery."""
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from users.models import Notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options): # noqa: D102
|
||||
logger.info('start, checking for outstanding notifications')
|
||||
notifications = Notification.objects.filter(processed_by_mailer_at=None)
|
||||
batched_by_recipient = defaultdict(list)
|
||||
for n in notifications:
|
||||
batched_by_recipient[n.recipient].append(n)
|
||||
|
||||
for recipient, batch in batched_by_recipient.items():
|
||||
to_send = []
|
||||
for n in batch:
|
||||
n.processed_by_mailer_at = timezone.now()
|
||||
if recipient.is_subscribed(n):
|
||||
n.sent = True
|
||||
to_send.append(n)
|
||||
# first mark, then send: to avoid spamming in case of a crash-loop
|
||||
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)
|
||||
logger.info('finish')
|
||||
|
||||
|
||||
def send_email(recipient, to_send):
|
||||
pass
|
26
users/migrations/0003_notification.py
Normal file
26
users/migrations/0003_notification.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.2.11 on 2024-04-12 12:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('actstream', '0003_add_follow_flag'),
|
||||
('users', '0002_moderators_group'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('processed_by_mailer_at', models.DateTimeField(default=None, null=True)),
|
||||
('sent', models.BooleanField(default=False)),
|
||||
('action', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='actstream.action')),
|
||||
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@ -3,6 +3,7 @@ from typing import Optional
|
||||
import logging
|
||||
import time
|
||||
|
||||
from actstream.models import Action
|
||||
from django.contrib.admin.utils import NestedObjects
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models, DEFAULT_DB_ALIAS, transaction
|
||||
@ -139,3 +140,13 @@ class User(TrackChangesMixin, AbstractUser):
|
||||
def is_moderator(self):
|
||||
# Used to review and approve extensions
|
||||
return self.groups.filter(name='moderators').exists()
|
||||
|
||||
def is_subscribed(self, notification: 'Notification') -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
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)
|
||||
|
@ -10,8 +10,9 @@ from django.dispatch import receiver
|
||||
|
||||
from blender_id_oauth_client import signals as bid_signals
|
||||
|
||||
from users.blender_id import BIDSession
|
||||
from extensions.models import Extension
|
||||
from users.blender_id import BIDSession
|
||||
from users.models import Notification
|
||||
|
||||
User = get_user_model()
|
||||
bid = BIDSession()
|
||||
@ -60,10 +61,7 @@ def create_notification(
|
||||
|
||||
audience = filter(lambda f: f != instance.actor, followers(instance.target))
|
||||
for recipient in audience:
|
||||
print(
|
||||
f'create notification for {recipient}: ',
|
||||
f'{instance.actor} {instance.verb} {instance.target}',
|
||||
)
|
||||
Notification.objects.get_or_create(recipient=recipient, action=instance)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=User.groups.through)
|
||||
|
Loading…
Reference in New Issue
Block a user