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 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)
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user