Skip to content
Snippets Groups Projects
Verified Commit df7a51c2 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Allow scheduling notifications by setting a send datetime

parent 370ec105
No related branches found
No related tags found
1 merge request!983Allow scheduling notifications by setting a send datetime
Pipeline #57526 failed
......@@ -19,6 +19,7 @@ Added
* Allow to configure if additional field is required
* Allow to configure description of additional fields
* Allow configuring regex for allowed usernames
* [Dev] Support scheduled notifications.
Changed
~~~~~~~
......
# Generated by Django 3.2.12 on 2022-02-23 19:33
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('core', '0036_additionalfields_helptext_required'),
]
operations = [
migrations.AddField(
model_name='notification',
name='send_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Send notification at'),
preserve_default=False,
),
]
......@@ -29,6 +29,7 @@ import customidenticon
import jsonstore
from cachalot.api import cachalot_disabled
from cache_memoize import cache_memoize
from celery.result import AsyncResult
from django_celery_results.models import TaskResult
from django_cte import CTEQuerySet, With
from dynamic_preferences.models import PerInstancePreferenceModel
......@@ -739,6 +740,8 @@ class Notification(ExtensibleModel, TimeStampedModel):
description = models.TextField(max_length=500, verbose_name=_("Description"))
link = models.URLField(blank=True, verbose_name=_("Link"))
send_at = models.DateTimeField(default=timezone.now, verbose_name=_("Send notification at"))
read = models.BooleanField(default=False, verbose_name=_("Read"))
sent = models.BooleanField(default=False, verbose_name=_("Sent"))
......@@ -747,10 +750,14 @@ class Notification(ExtensibleModel, TimeStampedModel):
def save(self, **kwargs):
super().save(**kwargs)
if not self.sent:
send_notification(self.pk, resend=True)
self.sent = True
super().save(**kwargs)
if not self.sent and self.send_at <= timezone.now():
self.send()
super().save(**kwargs)
def send(self, resend: bool = False) -> Optional[AsyncResult]:
"""Send the notification to the recipient."""
if not self.sent or resend:
return send_notification.delay(self.pk, resend=True)
class Meta:
verbose_name = _("Notification")
......
......@@ -4,6 +4,7 @@ from django.conf import settings
from django.core import management
from .celery import app
from .util.notifications import _send_due_notifications as _send_due_notifications
from .util.notifications import send_notification as _send_notification
......@@ -48,3 +49,9 @@ def clear_oauth_tokens():
from oauth2_provider.models import clear_expired # noqa
return clear_expired()
@app.task(run_every=timedelta(minutes=5))
def send_notifications():
"""Send due notifications to users."""
_send_due_notifications()
from datetime import timedelta
from time import sleep
from unittest.mock import patch
from django.test import override_settings
from django.utils import timezone
import pytest
from freezegun import freeze_time
from aleksis.core.models import Notification, Person
from aleksis.core.util.notifications import _send_due_notifications
pytestmark = pytest.mark.django_db
def test_email_notification(mailoutbox):
def test_send_notification():
email = "doe@example.com"
recipient = Person.objects.create(first_name="Jane", last_name="Doe", email=email)
sender = "Foo"
title = "There is happened something."
description = "Here you get some more information."
link = "https://aleksis.org/"
notification = Notification(
sender=sender,
recipient=recipient,
title=title,
description=description,
link=link,
)
with patch("aleksis.core.models.Notification.send") as patched_send:
patched_send.assert_not_called()
notification.save()
patched_send.assert_called()
def test_send_scheduled_notification():
email = "doe@example.com"
recipient = Person.objects.create(first_name="Jane", last_name="Doe", email=email)
sender = "Foo"
title = "There is happened something."
description = "Here you get some more information."
link = "https://aleksis.org/"
notification = Notification(
sender=sender,
recipient=recipient,
title=title,
description=description,
link=link,
send_at=timezone.now() + timedelta(days=1),
)
notification.save()
with patch("aleksis.core.models.Notification.send") as patched_send:
patched_send.assert_not_called()
_send_due_notifications()
patched_send.assert_not_called()
with freeze_time(timezone.now() + timedelta(days=1)):
_send_due_notifications()
patched_send.assert_called()
@override_settings(CELERY_BROKER_URL="memory://localhost//")
@pytest.mark.django_db(
databases=["default", "default_oot"], serialized_rollback=True, transaction=True
)
def test_email_notification(mailoutbox, celery_worker):
email = "doe@example.com"
recipient = Person.objects.create(first_name="Jane", last_name="Doe", email=email)
......@@ -19,6 +89,9 @@ def test_email_notification(mailoutbox):
)
notification.save()
sleep(3)
notification.refresh_from_db()
assert notification.sent
assert len(mailoutbox) == 1
......
......@@ -5,6 +5,7 @@ from typing import Sequence, Union
from django.apps import apps
from django.conf import settings
from django.template.loader import get_template
from django.utils import timezone
from django.utils.functional import lazy
from django.utils.translation import gettext_lazy as _
......@@ -82,6 +83,8 @@ def send_notification(notification: Union[int, "Notification"], resend: bool = F
name, check, send = _CHANNELS_MAP[channel]
if check():
send(notification)
notification.sent = True
notification.save()
def get_notification_choices() -> list:
......@@ -99,3 +102,12 @@ def get_notification_choices() -> list:
get_notification_choices_lazy = lazy(get_notification_choices, tuple)
def _send_due_notifications():
"""Send all notifications that are due to be sent."""
Notification = apps.get_model("core", "Notification")
due_notifications = Notification.objects.filter(sent=False, send_at__lte=timezone.now())
for notification in due_notifications:
notification.send()
......@@ -216,7 +216,9 @@ def index(request: HttpRequest) -> HttpResponse:
widgets = []
activities = person.activities.all().order_by("-created")[:5]
notifications = person.notifications.all().order_by("-created")[:5]
notifications = person.notifications.filter(send_at__lte=timezone.now()).order_by("-created")[
:5
]
unread_notifications = person.notifications.all().filter(read=False).order_by("-created")
context["activities"] = activities
......@@ -246,7 +248,9 @@ class NotificationsListView(PermissionRequiredMixin, ListView):
template_name = "core/notifications.html"
def get_queryset(self) -> QuerySet:
return self.request.user.person.notifications.order_by("-created")
return self.request.user.person.notifications.filter(send_at__lte=timezone.now()).order_by(
"-created"
)
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
self.get_queryset().filter(read=False).update(read=True)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment