Notification emails #80

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

View 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

View 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)),
],
),
]

View File

@ -3,6 +3,7 @@ from typing import Optional
import logging import logging
import time import time
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.db import models, DEFAULT_DB_ALIAS, transaction from django.db import models, DEFAULT_DB_ALIAS, transaction
@ -139,3 +140,13 @@ class User(TrackChangesMixin, AbstractUser):
def is_moderator(self): def is_moderator(self):
# Used to review and approve extensions # Used to review and approve extensions
return self.groups.filter(name='moderators').exists() 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)

View File

@ -10,8 +10,9 @@ from django.dispatch import receiver
from blender_id_oauth_client import signals as bid_signals from blender_id_oauth_client import signals as bid_signals
from users.blender_id import BIDSession
from extensions.models import Extension from extensions.models import Extension
from users.blender_id import BIDSession
from users.models import Notification
User = get_user_model() User = get_user_model()
bid = BIDSession() bid = BIDSession()
@ -60,10 +61,7 @@ def create_notification(
audience = filter(lambda f: f != instance.actor, followers(instance.target)) audience = filter(lambda f: f != instance.actor, followers(instance.target))
for recipient in audience: for recipient in audience:
print( Notification.objects.get_or_create(recipient=recipient, action=instance)
f'create notification for {recipient}: ',
f'{instance.actor} {instance.verb} {instance.target}',
)
@receiver(m2m_changed, sender=User.groups.through) @receiver(m2m_changed, sender=User.groups.through)