wbcore 1.61.4__py2.py3-none-any.whl → 1.61.6__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wbcore/contrib/authentication/admin.py +7 -1
- wbcore/contrib/notifications/admin.py +3 -10
- wbcore/contrib/notifications/backends/firebase/backends.py +2 -8
- wbcore/contrib/notifications/dispatch.py +21 -21
- wbcore/contrib/notifications/factories/notifications.py +1 -1
- wbcore/contrib/notifications/migrations/0010_notification_checksum_notification_checksum_unique.py +50 -0
- wbcore/contrib/notifications/models/notification_types.py +71 -76
- wbcore/contrib/notifications/models/notifications.py +86 -3
- wbcore/contrib/notifications/tests/conftest.py +2 -2
- wbcore/contrib/notifications/tests/test_models/test_notifications.py +111 -0
- wbcore/contrib/notifications/tests/test_viewsets/test_notifications.py +4 -4
- wbcore/contrib/notifications/utils.py +11 -0
- wbcore/contrib/permission/migrations/0001_initial.py +36 -85
- wbcore/contrib/permission/tasks.py +8 -2
- wbcore/serializers/fields/number.py +21 -5
- {wbcore-1.61.4.dist-info → wbcore-1.61.6.dist-info}/METADATA +1 -1
- {wbcore-1.61.4.dist-info → wbcore-1.61.6.dist-info}/RECORD +18 -19
- wbcore/contrib/notifications/tasks.py +0 -57
- wbcore/contrib/notifications/tests/test_tasks.py +0 -72
- {wbcore-1.61.4.dist-info → wbcore-1.61.6.dist-info}/WHEEL +0 -0
|
@@ -78,7 +78,13 @@ class UserAdmin(admin.ModelAdmin):
|
|
|
78
78
|
(_("Synchronization"), {"fields": ("metadata",)}),
|
|
79
79
|
(
|
|
80
80
|
_("Permissions"),
|
|
81
|
-
{
|
|
81
|
+
{
|
|
82
|
+
"fields": (
|
|
83
|
+
("is_active", "is_register", "is_staff", "is_superuser", "is_internal"),
|
|
84
|
+
("groups",),
|
|
85
|
+
("user_permissions",),
|
|
86
|
+
)
|
|
87
|
+
},
|
|
82
88
|
),
|
|
83
89
|
)
|
|
84
90
|
raw_id_fields = ("profile",)
|
|
@@ -2,7 +2,7 @@ from datetime import date, timedelta
|
|
|
2
2
|
|
|
3
3
|
from django.contrib import admin
|
|
4
4
|
|
|
5
|
-
from wbcore.contrib.notifications.
|
|
5
|
+
from wbcore.contrib.notifications.models.notifications import send_notification_as_task
|
|
6
6
|
|
|
7
7
|
from .models import (
|
|
8
8
|
Notification,
|
|
@@ -16,19 +16,12 @@ from .models import (
|
|
|
16
16
|
class NotificationModelAdmin(admin.ModelAdmin):
|
|
17
17
|
search_fields = ["title", "body"]
|
|
18
18
|
|
|
19
|
-
list_display = [
|
|
20
|
-
"title",
|
|
21
|
-
"user",
|
|
22
|
-
"notification_type",
|
|
23
|
-
"endpoint",
|
|
24
|
-
"sent",
|
|
25
|
-
"read",
|
|
26
|
-
]
|
|
19
|
+
list_display = ["title", "user", "notification_type", "endpoint", "sent", "read", "created"]
|
|
27
20
|
|
|
28
21
|
@admin.action(description="Send Notification")
|
|
29
22
|
def send_notification(self, request, queryset):
|
|
30
23
|
for notification in queryset:
|
|
31
|
-
|
|
24
|
+
send_notification_as_task.delay(notification.id)
|
|
32
25
|
|
|
33
26
|
actions = [send_notification]
|
|
34
27
|
|
|
@@ -11,9 +11,6 @@ from wbcore.contrib.notifications.backends.abstract_backend import (
|
|
|
11
11
|
AbstractNotificationBackend,
|
|
12
12
|
)
|
|
13
13
|
from wbcore.contrib.notifications.models import Notification, NotificationUserToken
|
|
14
|
-
from wbcore.contrib.notifications.models.notification_types import (
|
|
15
|
-
NotificationTypeSetting,
|
|
16
|
-
)
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
class NotificationBackend(AbstractNotificationBackend):
|
|
@@ -34,11 +31,8 @@ class NotificationBackend(AbstractNotificationBackend):
|
|
|
34
31
|
@classmethod
|
|
35
32
|
def send_notification(cls, notification: Notification):
|
|
36
33
|
app = cls.get_firebase_app(cls.get_firebase_credentials())
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
user=notification.user,
|
|
40
|
-
)
|
|
41
|
-
tokens = NotificationUserToken.objects.filter_for_user_settings(notification_user_settings)
|
|
34
|
+
notification_user_setting = notification.notification_type.get_setting_for_user(notification.user)
|
|
35
|
+
tokens = NotificationUserToken.objects.filter_for_user_settings(notification_user_setting)
|
|
42
36
|
endpoint_data = {} # Firebase can't accept non-string value
|
|
43
37
|
if full_endpoint := notification.get_full_endpoint():
|
|
44
38
|
endpoint_data["endpoint"] = full_endpoint
|
|
@@ -4,7 +4,6 @@ from celery import shared_task
|
|
|
4
4
|
from django.conf import settings
|
|
5
5
|
from django.db import transaction
|
|
6
6
|
from django.dispatch import receiver
|
|
7
|
-
from django.utils import timezone
|
|
8
7
|
from django.utils.module_loading import import_string
|
|
9
8
|
from django.utils.translation import gettext
|
|
10
9
|
from rest_framework.reverse import reverse
|
|
@@ -13,15 +12,24 @@ from wbcore.contrib.authentication.models.users import User
|
|
|
13
12
|
from wbcore.shares.signals import handle_widget_sharing
|
|
14
13
|
|
|
15
14
|
from ...workers import Queue
|
|
16
|
-
from .models import
|
|
17
|
-
from .
|
|
15
|
+
from .models import NotificationType
|
|
16
|
+
from .models.notifications import Notification, send_notification_as_task
|
|
18
17
|
|
|
19
18
|
|
|
19
|
+
def get_user(u: User | int) -> User:
|
|
20
|
+
if isinstance(u, User):
|
|
21
|
+
return u
|
|
22
|
+
elif isinstance(u, int):
|
|
23
|
+
return User.objects.get(id=u)
|
|
24
|
+
raise ValueError("Invalid user type")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@shared_task(queue=Queue.HIGH_PRIORITY.value)
|
|
20
28
|
def send_notification(
|
|
21
29
|
code: str,
|
|
22
30
|
title: str,
|
|
23
31
|
body: str,
|
|
24
|
-
user: User | Iterable[User],
|
|
32
|
+
user: User | Iterable[User | int] | int,
|
|
25
33
|
reverse_name: str | None = None,
|
|
26
34
|
reverse_args=None,
|
|
27
35
|
reverse_kwargs=None,
|
|
@@ -39,15 +47,16 @@ def send_notification(
|
|
|
39
47
|
reverse_kwargs: The keyword arguments passed to the `reverse` function
|
|
40
48
|
endpoint: The endpoint of resource. If provided, reverse_name + args + kwargs are disregarded
|
|
41
49
|
"""
|
|
42
|
-
users =
|
|
43
|
-
if isinstance(
|
|
44
|
-
|
|
50
|
+
users = []
|
|
51
|
+
if isinstance(user, list):
|
|
52
|
+
for u in user:
|
|
53
|
+
users.append(get_user(u))
|
|
54
|
+
else:
|
|
55
|
+
users.append(get_user(user))
|
|
56
|
+
|
|
45
57
|
for user in users:
|
|
46
58
|
notification_type = NotificationType.objects.get(code=code)
|
|
47
|
-
if
|
|
48
|
-
user.is_active
|
|
49
|
-
and NotificationTypeSetting.objects.filter(notification_type=notification_type, user=user).exists()
|
|
50
|
-
):
|
|
59
|
+
if user.is_active:
|
|
51
60
|
if not endpoint:
|
|
52
61
|
endpoint = reverse(reverse_name, reverse_args, reverse_kwargs) if reverse_name else None
|
|
53
62
|
notification = Notification.objects.create(
|
|
@@ -56,21 +65,12 @@ def send_notification(
|
|
|
56
65
|
user=user,
|
|
57
66
|
notification_type=notification_type,
|
|
58
67
|
endpoint=endpoint,
|
|
59
|
-
sent=timezone.now(),
|
|
60
68
|
)
|
|
61
69
|
transaction.on_commit(
|
|
62
|
-
lambda notification_pk=notification.pk:
|
|
70
|
+
lambda notification_pk=notification.pk: send_notification_as_task.delay(notification_pk)
|
|
63
71
|
)
|
|
64
72
|
|
|
65
73
|
|
|
66
|
-
@shared_task(queue=Queue.HIGH_PRIORITY.value)
|
|
67
|
-
def send_notification_as_task(code, title, body, user_id, **kwargs):
|
|
68
|
-
if not isinstance(user_id, list):
|
|
69
|
-
user_id = [user_id]
|
|
70
|
-
user = User.objects.filter(id__in=user_id)
|
|
71
|
-
send_notification(code, title, body, user, **kwargs)
|
|
72
|
-
|
|
73
|
-
|
|
74
74
|
@receiver(handle_widget_sharing)
|
|
75
75
|
def share_notification(
|
|
76
76
|
request, widget_relative_endpoint, share=False, share_message=None, share_recipients=None, **kwargs
|
|
@@ -6,7 +6,7 @@ from wbcore.contrib.notifications.factories.notification_types import (
|
|
|
6
6
|
from wbcore.contrib.notifications.models import Notification
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class
|
|
9
|
+
class NotificationFactory(factory.django.DjangoModelFactory):
|
|
10
10
|
title = factory.Faker("pystr")
|
|
11
11
|
body = factory.Faker("pystr")
|
|
12
12
|
notification_type = factory.SubFactory(NotificationTypeModelFactory)
|
wbcore/contrib/notifications/migrations/0010_notification_checksum_notification_checksum_unique.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generated by Django 5.2.9 on 2026-02-02 11:27
|
|
2
|
+
import hashlib
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
|
|
8
|
+
from wbcore.contrib.notifications.utils import get_checksum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def migrate_checksum(apps, schema_editor):
|
|
12
|
+
Notification = apps.get_model('notifications', 'Notification')
|
|
13
|
+
qs = Notification.objects.all()
|
|
14
|
+
objs = []
|
|
15
|
+
for notification in tqdm(qs, total=qs.count()):
|
|
16
|
+
notification.checksum = get_checksum(notification.title, notification.body, notification.endpoint)
|
|
17
|
+
objs.append(notification)
|
|
18
|
+
Notification.objects.bulk_update(objs, ["checksum"], batch_size=1000)
|
|
19
|
+
|
|
20
|
+
class Migration(migrations.Migration):
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
('notifications', '0009_alter_notificationtypesetting_options_and_more'),
|
|
24
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
operations = [
|
|
28
|
+
migrations.AddField(
|
|
29
|
+
model_name='notification',
|
|
30
|
+
name='checksum',
|
|
31
|
+
field=models.CharField(null=True, blank=True, max_length=64),
|
|
32
|
+
preserve_default=False,
|
|
33
|
+
),
|
|
34
|
+
migrations.RunPython(migrate_checksum),
|
|
35
|
+
migrations.AlterField(
|
|
36
|
+
model_name='notification',
|
|
37
|
+
name='body',
|
|
38
|
+
field=models.TextField(default=''),
|
|
39
|
+
),
|
|
40
|
+
migrations.AlterField(
|
|
41
|
+
model_name='notification',
|
|
42
|
+
name='checksum',
|
|
43
|
+
field=models.CharField(default=None, max_length=64),
|
|
44
|
+
preserve_default=False,
|
|
45
|
+
),
|
|
46
|
+
migrations.AddIndex(
|
|
47
|
+
model_name='notification',
|
|
48
|
+
index=models.Index(fields=['user', 'notification_type', 'checksum'], name='notificatio_user_id_b90c58_idx'),
|
|
49
|
+
),
|
|
50
|
+
]
|
|
@@ -10,54 +10,6 @@ from wbcore.contrib.authentication.models.users import User
|
|
|
10
10
|
from wbcore.models import WBModel
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class NotificationType(WBModel):
|
|
14
|
-
code = models.CharField(max_length=128, unique=True)
|
|
15
|
-
title = models.CharField(max_length=128, default="")
|
|
16
|
-
help_text = models.CharField(max_length=512, default="")
|
|
17
|
-
|
|
18
|
-
stale = models.BooleanField(default=False)
|
|
19
|
-
contenttype = models.ForeignKey(
|
|
20
|
-
to="contenttypes.ContentType", related_name="+", null=True, blank=True, on_delete=models.SET_NULL
|
|
21
|
-
)
|
|
22
|
-
resource_button_label = models.CharField(max_length=128)
|
|
23
|
-
|
|
24
|
-
default_enable_web = models.BooleanField(default=False)
|
|
25
|
-
default_enable_mobile = models.BooleanField(default=False)
|
|
26
|
-
default_enable_email = models.BooleanField(default=False)
|
|
27
|
-
is_lock = models.BooleanField(default=False)
|
|
28
|
-
|
|
29
|
-
def __str__(self) -> str:
|
|
30
|
-
return f"{self.title}"
|
|
31
|
-
|
|
32
|
-
def save(self, *args, **kwargs):
|
|
33
|
-
if not self.resource_button_label:
|
|
34
|
-
if self.contenttype:
|
|
35
|
-
self.resource_button_label = f"Open {self.contenttype.name}"
|
|
36
|
-
else:
|
|
37
|
-
self.resource_button_label = "Open Resource"
|
|
38
|
-
super().save(*args, **kwargs)
|
|
39
|
-
|
|
40
|
-
class Meta:
|
|
41
|
-
verbose_name = "Notification Type"
|
|
42
|
-
verbose_name_plural = "Notification Types"
|
|
43
|
-
|
|
44
|
-
@classmethod
|
|
45
|
-
def get_endpoint_basename(cls):
|
|
46
|
-
return "wbcore:notifications:notification_type"
|
|
47
|
-
|
|
48
|
-
@classmethod
|
|
49
|
-
def get_representation_endpoint(cls):
|
|
50
|
-
return "wbcore:notifications:notification_type_representation-list"
|
|
51
|
-
|
|
52
|
-
@classmethod
|
|
53
|
-
def get_representation_value_key(cls):
|
|
54
|
-
return "id"
|
|
55
|
-
|
|
56
|
-
@classmethod
|
|
57
|
-
def get_representation_label_key(cls):
|
|
58
|
-
return "{{title}}"
|
|
59
|
-
|
|
60
|
-
|
|
61
13
|
class NotificationTypeSetting(WBModel):
|
|
62
14
|
notification_type = models.ForeignKey(
|
|
63
15
|
to="notifications.NotificationType", related_name="user_settings", on_delete=models.CASCADE
|
|
@@ -106,31 +58,81 @@ class NotificationTypeSetting(WBModel):
|
|
|
106
58
|
return "{{notification_type}}"
|
|
107
59
|
|
|
108
60
|
|
|
61
|
+
class NotificationType(WBModel):
|
|
62
|
+
code = models.CharField(max_length=128, unique=True)
|
|
63
|
+
title = models.CharField(max_length=128, default="")
|
|
64
|
+
help_text = models.CharField(max_length=512, default="")
|
|
65
|
+
|
|
66
|
+
stale = models.BooleanField(default=False)
|
|
67
|
+
contenttype = models.ForeignKey(
|
|
68
|
+
to="contenttypes.ContentType", related_name="+", null=True, blank=True, on_delete=models.SET_NULL
|
|
69
|
+
)
|
|
70
|
+
resource_button_label = models.CharField(max_length=128)
|
|
71
|
+
|
|
72
|
+
default_enable_web = models.BooleanField(default=False)
|
|
73
|
+
default_enable_mobile = models.BooleanField(default=False)
|
|
74
|
+
default_enable_email = models.BooleanField(default=False)
|
|
75
|
+
is_lock = models.BooleanField(default=False)
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
return f"{self.title}"
|
|
79
|
+
|
|
80
|
+
def save(self, *args, **kwargs):
|
|
81
|
+
if not self.resource_button_label:
|
|
82
|
+
if self.contenttype:
|
|
83
|
+
self.resource_button_label = f"Open {self.contenttype.name}"
|
|
84
|
+
else:
|
|
85
|
+
self.resource_button_label = "Open Resource"
|
|
86
|
+
super().save(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
class Meta:
|
|
89
|
+
verbose_name = "Notification Type"
|
|
90
|
+
verbose_name_plural = "Notification Types"
|
|
91
|
+
|
|
92
|
+
def get_setting_for_user(self, user: User, bulk: bool = False) -> NotificationTypeSetting:
|
|
93
|
+
try:
|
|
94
|
+
setting = NotificationTypeSetting.objects.get(user=user, notification_type=self)
|
|
95
|
+
setting.enable_web = setting.enable_web if not self.is_lock else self.default_enable_web
|
|
96
|
+
setting.enable_mobile = setting.enable_mobile if not self.is_lock else self.default_enable_mobile
|
|
97
|
+
setting.enable_email = setting.enable_email if not self.is_lock else self.default_enable_email
|
|
98
|
+
return setting
|
|
99
|
+
except NotificationTypeSetting.DoesNotExist:
|
|
100
|
+
setting = NotificationTypeSetting(
|
|
101
|
+
user=user,
|
|
102
|
+
notification_type=self,
|
|
103
|
+
enable_web=self.default_enable_web,
|
|
104
|
+
enable_mobile=self.default_enable_mobile,
|
|
105
|
+
enable_email=self.default_enable_email,
|
|
106
|
+
)
|
|
107
|
+
if not bulk:
|
|
108
|
+
setting.save()
|
|
109
|
+
return setting
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def get_endpoint_basename(cls):
|
|
113
|
+
return "wbcore:notifications:notification_type"
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def get_representation_endpoint(cls):
|
|
117
|
+
return "wbcore:notifications:notification_type_representation-list"
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def get_representation_value_key(cls):
|
|
121
|
+
return "id"
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def get_representation_label_key(cls):
|
|
125
|
+
return "{{title}}"
|
|
126
|
+
|
|
127
|
+
|
|
109
128
|
@receiver(post_save, sender="notifications.NotificationType")
|
|
110
129
|
def post_save_notification_type(instance, **kwargs):
|
|
111
130
|
anonymous_user = get_anonymous_user()
|
|
112
131
|
|
|
113
132
|
objs = []
|
|
114
133
|
for user in User.objects.filter(~models.Q(pk=anonymous_user.pk)):
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
enable_web = existing_setting.enable_web if not instance.is_lock else instance.default_enable_web
|
|
118
|
-
enable_mobile = existing_setting.enable_mobile if not instance.is_lock else instance.default_enable_mobile
|
|
119
|
-
enable_email = existing_setting.enable_email if not instance.is_lock else instance.default_enable_email
|
|
120
|
-
except NotificationTypeSetting.DoesNotExist:
|
|
121
|
-
enable_web = instance.default_enable_web
|
|
122
|
-
enable_mobile = instance.default_enable_mobile
|
|
123
|
-
enable_email = instance.default_enable_email
|
|
124
|
-
|
|
125
|
-
objs.append(
|
|
126
|
-
NotificationTypeSetting(
|
|
127
|
-
notification_type=instance,
|
|
128
|
-
user=user,
|
|
129
|
-
enable_web=enable_web,
|
|
130
|
-
enable_mobile=enable_mobile,
|
|
131
|
-
enable_email=enable_email,
|
|
132
|
-
)
|
|
133
|
-
)
|
|
134
|
+
setting = instance.get_setting_for_user(user, bulk=True)
|
|
135
|
+
objs.append(setting)
|
|
134
136
|
if objs:
|
|
135
137
|
NotificationTypeSetting.objects.bulk_create(
|
|
136
138
|
objs,
|
|
@@ -150,14 +152,7 @@ def post_save_user(sender, instance, created, raw, **kwargs):
|
|
|
150
152
|
|
|
151
153
|
objs = []
|
|
152
154
|
for notification_type in NotificationType.objects.all():
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
notification_type=notification_type,
|
|
156
|
-
user=instance,
|
|
157
|
-
enable_web=notification_type.default_enable_web,
|
|
158
|
-
enable_mobile=notification_type.default_enable_mobile,
|
|
159
|
-
enable_email=notification_type.default_enable_email,
|
|
160
|
-
)
|
|
161
|
-
)
|
|
155
|
+
setting = notification_type.get_setting_for_user(instance, bulk=True)
|
|
156
|
+
objs.append(setting)
|
|
162
157
|
if objs:
|
|
163
158
|
NotificationTypeSetting.objects.bulk_create(objs, unique_fields=["notification_type", "user"])
|
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
import urllib
|
|
2
2
|
from contextlib import suppress
|
|
3
|
+
from datetime import timedelta
|
|
3
4
|
|
|
5
|
+
from celery import shared_task
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.core.mail import EmailMultiAlternatives
|
|
4
8
|
from django.core.validators import URLValidator, ValidationError
|
|
5
9
|
from django.db import models
|
|
10
|
+
from django.template.loader import get_template
|
|
6
11
|
from django.urls import Resolver404, resolve
|
|
12
|
+
from django.utils import timezone
|
|
7
13
|
from django.utils.functional import cached_property
|
|
14
|
+
from django.utils.html import strip_tags
|
|
15
|
+
from django.utils.module_loading import import_string
|
|
8
16
|
|
|
9
17
|
from wbcore.contrib.authentication.models.users import User
|
|
10
|
-
from wbcore.contrib.notifications.utils import base_domain, create_notification_type
|
|
18
|
+
from wbcore.contrib.notifications.utils import base_domain, create_notification_type, get_checksum
|
|
19
|
+
from wbcore.workers import Queue
|
|
11
20
|
|
|
12
21
|
|
|
13
22
|
class Notification(models.Model):
|
|
14
23
|
title = models.CharField(max_length=255)
|
|
15
|
-
body = models.TextField(
|
|
24
|
+
body = models.TextField(default="")
|
|
25
|
+
endpoint = models.CharField(max_length=2048, null=True, blank=True)
|
|
16
26
|
|
|
17
27
|
user = models.ForeignKey(to=User, related_name="notifications_notifications", on_delete=models.CASCADE)
|
|
18
28
|
notification_type = models.ForeignKey(
|
|
19
29
|
to="notifications.NotificationType", related_name="notifications", on_delete=models.CASCADE
|
|
20
30
|
)
|
|
21
|
-
endpoint = models.CharField(max_length=2048, null=True, blank=True)
|
|
22
31
|
|
|
23
32
|
created = models.DateTimeField(auto_now_add=True)
|
|
24
33
|
sent = models.DateTimeField(null=True, blank=True)
|
|
25
34
|
read = models.DateTimeField(null=True, blank=True)
|
|
35
|
+
checksum = models.CharField(max_length=64)
|
|
26
36
|
|
|
27
37
|
def __str__(self) -> str:
|
|
28
38
|
return f"{self.user} {self.title}"
|
|
@@ -41,6 +51,12 @@ class Notification(models.Model):
|
|
|
41
51
|
False,
|
|
42
52
|
),
|
|
43
53
|
]
|
|
54
|
+
indexes = [
|
|
55
|
+
models.Index(fields=["user", "notification_type", "checksum"]),
|
|
56
|
+
]
|
|
57
|
+
# constraints = [
|
|
58
|
+
# models.UniqueConstraint(fields=["user", "checksum"], name="checksum_unique"),
|
|
59
|
+
# ]
|
|
44
60
|
|
|
45
61
|
@cached_property
|
|
46
62
|
def is_endpoint_internal(self) -> bool:
|
|
@@ -60,6 +76,61 @@ class Notification(models.Model):
|
|
|
60
76
|
except ValidationError:
|
|
61
77
|
return False
|
|
62
78
|
|
|
79
|
+
def _send_as_mail(self):
|
|
80
|
+
"""Sends out a notification to the user specified inside the notification"""
|
|
81
|
+
|
|
82
|
+
context = {
|
|
83
|
+
"title": self.title,
|
|
84
|
+
"message": self.body or "",
|
|
85
|
+
"notification_share_url": self.get_full_endpoint(as_shareable_internal_link=True),
|
|
86
|
+
"notification_endpoint": self.get_full_endpoint(),
|
|
87
|
+
}
|
|
88
|
+
rendered_template = get_template("notifications/notification_template.html").render(context)
|
|
89
|
+
msg = EmailMultiAlternatives(
|
|
90
|
+
subject=self.title,
|
|
91
|
+
body=strip_tags(rendered_template),
|
|
92
|
+
from_email=getattr(settings, "WBCORE_NOTIFICATION_EMAIL_FROM", "no_reply@stainly.com"),
|
|
93
|
+
to=[self.user.email], # type: ignore
|
|
94
|
+
)
|
|
95
|
+
msg.attach_alternative(rendered_template, "text/html")
|
|
96
|
+
msg.send()
|
|
97
|
+
|
|
98
|
+
def save(self, *args, **kwargs):
|
|
99
|
+
if not self.checksum:
|
|
100
|
+
self.checksum = self.get_checksum()
|
|
101
|
+
super().save(*args, **kwargs)
|
|
102
|
+
|
|
103
|
+
def has_duplicated(self, interval: int = 60 * 4) -> bool:
|
|
104
|
+
return (
|
|
105
|
+
Notification.objects.exclude(id=self.id)
|
|
106
|
+
.filter(
|
|
107
|
+
user=self.user,
|
|
108
|
+
notification_type=self.notification_type,
|
|
109
|
+
checksum=self.checksum,
|
|
110
|
+
sent__isnull=False,
|
|
111
|
+
created__gt=self.created - timedelta(minutes=interval),
|
|
112
|
+
created__lt=self.created + timedelta(minutes=interval),
|
|
113
|
+
)
|
|
114
|
+
.exists()
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def send(self):
|
|
118
|
+
notification_user_setting = self.notification_type.get_setting_for_user(self.user)
|
|
119
|
+
|
|
120
|
+
# we do not sent notification through email if we detect similar already sent notification
|
|
121
|
+
if notification_user_setting.enable_email and not self.has_duplicated():
|
|
122
|
+
self._send_as_mail()
|
|
123
|
+
if notification_user_setting.enable_web or notification_user_setting.enable_mobile:
|
|
124
|
+
backend = import_string(settings.NOTIFICATION_BACKEND)
|
|
125
|
+
backend.send_notification(self)
|
|
126
|
+
|
|
127
|
+
# mark this notification as sent
|
|
128
|
+
self.sent = timezone.now()
|
|
129
|
+
self.save()
|
|
130
|
+
|
|
131
|
+
def get_checksum(self) -> str:
|
|
132
|
+
return get_checksum(self.title, self.body, self.endpoint)
|
|
133
|
+
|
|
63
134
|
def get_full_endpoint(self, as_shareable_internal_link: bool = False) -> str | None:
|
|
64
135
|
if self.is_endpoint_internal:
|
|
65
136
|
if as_shareable_internal_link:
|
|
@@ -80,3 +151,15 @@ class Notification(models.Model):
|
|
|
80
151
|
@classmethod
|
|
81
152
|
def get_representation_label_key(cls) -> str:
|
|
82
153
|
return "{{title}}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@shared_task(queue=Queue.HIGH_PRIORITY.value)
|
|
157
|
+
def send_notification_as_task(notification_pk: int):
|
|
158
|
+
"""A celery task to send out a notification via email, web or mobile
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
notification_pk: The primary key of the notification that is going to be send out
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
notification = Notification.objects.get(pk=notification_pk)
|
|
165
|
+
notification.send()
|
|
@@ -8,7 +8,7 @@ from wbcore.contrib.notifications.factories.notification_types import (
|
|
|
8
8
|
NotificationTypeSettingModelFactory,
|
|
9
9
|
)
|
|
10
10
|
from wbcore.contrib.notifications.factories.notifications import (
|
|
11
|
-
|
|
11
|
+
NotificationFactory,
|
|
12
12
|
)
|
|
13
13
|
from wbcore.contrib.notifications.factories.tokens import (
|
|
14
14
|
NotificationUserTokenModelFactory,
|
|
@@ -19,7 +19,7 @@ register(UserFactory)
|
|
|
19
19
|
register(PersonFactory)
|
|
20
20
|
register(NotificationTypeModelFactory, name="notification_type")
|
|
21
21
|
register(NotificationTypeSettingModelFactory, name="notification_type_setting")
|
|
22
|
-
register(
|
|
22
|
+
register(NotificationFactory)
|
|
23
23
|
register(NotificationUserTokenModelFactory, name="notification_user_token")
|
|
24
24
|
|
|
25
25
|
|
|
@@ -1,9 +1,38 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
1
4
|
import pytest
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
from django.core import mail
|
|
7
|
+
from django.test.utils import override_settings
|
|
2
8
|
|
|
3
9
|
from wbcore.contrib.notifications.models import Notification
|
|
4
10
|
from wbcore.contrib.notifications.utils import base_domain
|
|
5
11
|
|
|
6
12
|
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def patched_import_string(mocker):
|
|
15
|
+
return mocker.patch("wbcore.contrib.notifications.models.notifications.import_string")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_backend():
|
|
20
|
+
from wbcore.contrib.notifications.backends.abstract_backend import (
|
|
21
|
+
AbstractNotificationBackend,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
class MockedBackend(AbstractNotificationBackend):
|
|
25
|
+
@classmethod
|
|
26
|
+
def send_notification(cls, notification):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_configuration(cls) -> dict:
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
return MockedBackend
|
|
34
|
+
|
|
35
|
+
|
|
7
36
|
@pytest.mark.django_db
|
|
8
37
|
class TestNotification:
|
|
9
38
|
def test_factory(self, notification: Notification):
|
|
@@ -22,6 +51,23 @@ class TestNotification:
|
|
|
22
51
|
def test_representation_label_key(self):
|
|
23
52
|
assert Notification.get_representation_label_key() == "{{title}}"
|
|
24
53
|
|
|
54
|
+
def test_has_duplicated(self, notification_factory):
|
|
55
|
+
notification = notification_factory.create()
|
|
56
|
+
assert notification.has_duplicated() is False
|
|
57
|
+
duplicated_notification = notification_factory.create(
|
|
58
|
+
user=notification.user,
|
|
59
|
+
title=notification.title,
|
|
60
|
+
body=notification.body,
|
|
61
|
+
endpoint=notification.endpoint,
|
|
62
|
+
notification_type=notification.notification_type,
|
|
63
|
+
created=notification.created + timedelta(minutes=60),
|
|
64
|
+
)
|
|
65
|
+
assert notification.has_duplicated(interval=61) is False
|
|
66
|
+
assert notification.has_duplicated(interval=59) is False
|
|
67
|
+
duplicated_notification.sent = duplicated_notification.created
|
|
68
|
+
duplicated_notification.save()
|
|
69
|
+
assert notification.has_duplicated(interval=59) is True
|
|
70
|
+
|
|
25
71
|
@pytest.mark.parametrize("notification__endpoint", ["/wbcore/notifications/"])
|
|
26
72
|
def test_full_valid_internal_endpoint(self, notification):
|
|
27
73
|
assert notification.get_full_endpoint() == f"{base_domain()}{notification.endpoint}"
|
|
@@ -44,3 +90,68 @@ class TestNotification:
|
|
|
44
90
|
@pytest.mark.parametrize("notification__endpoint", ["https.www.google.com"])
|
|
45
91
|
def test_full_invalid_external_endpoint(self, notification):
|
|
46
92
|
assert notification.get_full_endpoint() is None
|
|
93
|
+
|
|
94
|
+
@patch.object(Notification, "_send_as_mail")
|
|
95
|
+
@patch.object(Notification, "has_duplicated")
|
|
96
|
+
def test_send_exclude_mail_with_duplicate(self, mock_has_duplicated, mock_send_as_mail, notification):
|
|
97
|
+
mock_has_duplicated.return_value = False
|
|
98
|
+
setting = notification.notification_type.get_setting_for_user(notification.user)
|
|
99
|
+
setting.enable_email = True
|
|
100
|
+
setting.save()
|
|
101
|
+
|
|
102
|
+
assert mock_send_as_mail.call_count == 0
|
|
103
|
+
notification.send()
|
|
104
|
+
assert mock_send_as_mail.call_count == 1
|
|
105
|
+
|
|
106
|
+
mock_has_duplicated.return_value = True
|
|
107
|
+
notification.send()
|
|
108
|
+
assert mock_send_as_mail.call_count == 1
|
|
109
|
+
|
|
110
|
+
@patch.object(Notification, "_send_as_mail")
|
|
111
|
+
def test_send_notification_task(
|
|
112
|
+
self, patched_send_notification_email, notification, mock_backend, mocker, patched_import_string
|
|
113
|
+
):
|
|
114
|
+
spy = mocker.spy(mock_backend, "send_notification")
|
|
115
|
+
patched_import_string.return_value = mock_backend
|
|
116
|
+
|
|
117
|
+
setting = notification.notification_type.get_setting_for_user(notification.user)
|
|
118
|
+
setting.enable_email = False
|
|
119
|
+
setting.save()
|
|
120
|
+
|
|
121
|
+
notification.send()
|
|
122
|
+
patched_send_notification_email.assert_not_called()
|
|
123
|
+
|
|
124
|
+
setting.enable_email = True
|
|
125
|
+
setting.save()
|
|
126
|
+
notification.send()
|
|
127
|
+
patched_send_notification_email.assert_called_once_with()
|
|
128
|
+
|
|
129
|
+
setting.enable_web = True
|
|
130
|
+
setting.save()
|
|
131
|
+
notification.send()
|
|
132
|
+
patched_import_string.assert_called_once()
|
|
133
|
+
spy.assert_called_once_with(notification)
|
|
134
|
+
|
|
135
|
+
@patch.object(Notification, "_send_as_mail")
|
|
136
|
+
def test_send_notification_task_without_mail(
|
|
137
|
+
self, patched_send_notification_email, notification, mock_backend, patched_import_string
|
|
138
|
+
):
|
|
139
|
+
notification.user.wbnotification_user_settings.update(enable_email=False)
|
|
140
|
+
patched_import_string.return_value = mock_backend
|
|
141
|
+
|
|
142
|
+
notification.send()
|
|
143
|
+
|
|
144
|
+
assert not patched_send_notification_email.called
|
|
145
|
+
|
|
146
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
|
147
|
+
@pytest.mark.parametrize("notification__endpoint", ["/wbcore/notifications/"])
|
|
148
|
+
def test_send_notification_email(self, notification):
|
|
149
|
+
setting = notification.notification_type.get_setting_for_user(notification.user)
|
|
150
|
+
setting.enable_email = True
|
|
151
|
+
setting.save()
|
|
152
|
+
|
|
153
|
+
assert len(mail.outbox) == 0
|
|
154
|
+
notification.send()
|
|
155
|
+
assert len(mail.outbox) == 1
|
|
156
|
+
soup = BeautifulSoup(mail.outbox[0].alternatives[0][0], "html.parser")
|
|
157
|
+
assert soup.find("a", href=notification.get_full_endpoint(as_shareable_internal_link=True))
|
|
@@ -4,7 +4,7 @@ from rest_framework.reverse import reverse
|
|
|
4
4
|
|
|
5
5
|
from wbcore.contrib.authentication.factories import UserFactory
|
|
6
6
|
from wbcore.contrib.notifications.factories.notifications import (
|
|
7
|
-
|
|
7
|
+
NotificationFactory,
|
|
8
8
|
)
|
|
9
9
|
from wbcore.contrib.notifications.models.notifications import Notification
|
|
10
10
|
|
|
@@ -109,7 +109,7 @@ class TestNotificationModelViewSet:
|
|
|
109
109
|
|
|
110
110
|
@pytest.mark.parametrize("user__user_permissions", [(["notifications.change_notification"])])
|
|
111
111
|
def test_read_all_action_other_notification(self, notification, client, user):
|
|
112
|
-
notification2 =
|
|
112
|
+
notification2 = NotificationFactory()
|
|
113
113
|
assert notification.read is None
|
|
114
114
|
assert notification2.read is None # type: ignore
|
|
115
115
|
client.force_authenticate(user)
|
|
@@ -121,7 +121,7 @@ class TestNotificationModelViewSet:
|
|
|
121
121
|
|
|
122
122
|
@pytest.mark.parametrize("user__user_permissions", [(["notifications.change_notification"])])
|
|
123
123
|
def test_delete_all_action(self, notification, client, user):
|
|
124
|
-
|
|
124
|
+
NotificationFactory(user=user, read=timezone.now())
|
|
125
125
|
assert Notification.objects.filter(user=user).count() == 2
|
|
126
126
|
|
|
127
127
|
client.force_authenticate(user)
|
|
@@ -132,7 +132,7 @@ class TestNotificationModelViewSet:
|
|
|
132
132
|
@pytest.mark.parametrize("user__user_permissions", [(["notifications.change_notification"])])
|
|
133
133
|
def test_delete_all_action_other_notification(self, notification, client, user):
|
|
134
134
|
user2 = UserFactory()
|
|
135
|
-
|
|
135
|
+
NotificationFactory(user=user2, read=timezone.now())
|
|
136
136
|
notification.read = timezone.now()
|
|
137
137
|
notification.save()
|
|
138
138
|
assert Notification.objects.all().count() == 2
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
|
|
1
3
|
from django.conf import settings
|
|
2
4
|
from django.contrib.sites.models import Site
|
|
3
5
|
|
|
4
6
|
|
|
7
|
+
def get_checksum(title: str, body: str | None, endpoint: str | None) -> str:
|
|
8
|
+
content = title
|
|
9
|
+
if body:
|
|
10
|
+
content += ":" + body
|
|
11
|
+
if endpoint:
|
|
12
|
+
content += ":" + endpoint
|
|
13
|
+
return hashlib.sha256(content.encode()).hexdigest()
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
def base_domain() -> str:
|
|
6
17
|
"""A utility method that assembles the current domain. Utilizes the site app from django
|
|
7
18
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 5.
|
|
1
|
+
# Generated by Django 5.2.9 on 2026-01-28 14:41
|
|
2
2
|
|
|
3
3
|
import django.db.models.deletion
|
|
4
4
|
from django.conf import settings
|
|
@@ -6,98 +6,49 @@ from django.db import migrations, models
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Migration(migrations.Migration):
|
|
9
|
+
|
|
9
10
|
initial = True
|
|
10
11
|
|
|
11
12
|
dependencies = [
|
|
12
|
-
(
|
|
13
|
-
(
|
|
13
|
+
('auth', '0012_alter_user_first_name_max_length'),
|
|
14
|
+
('contenttypes', '0002_remove_content_type_name'),
|
|
14
15
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
16
|
]
|
|
16
17
|
|
|
17
18
|
operations = [
|
|
18
|
-
migrations.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
"content_type",
|
|
29
|
-
models.ForeignKey(
|
|
30
|
-
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
|
|
31
|
-
),
|
|
32
|
-
),
|
|
33
|
-
("group", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="auth.group")),
|
|
34
|
-
(
|
|
35
|
-
"permission",
|
|
36
|
-
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="auth.permission"),
|
|
37
|
-
),
|
|
38
|
-
],
|
|
39
|
-
options={
|
|
40
|
-
"db_table": "bridger_biggroupobjectpermission",
|
|
41
|
-
"abstract": False,
|
|
42
|
-
"default_related_name": "groupobjectpermissions",
|
|
43
|
-
"unique_together": {("group", "permission", "object_pk")},
|
|
44
|
-
},
|
|
45
|
-
),
|
|
46
|
-
migrations.CreateModel(
|
|
47
|
-
name="UserObjectPermission",
|
|
48
|
-
fields=[
|
|
49
|
-
("object_pk", models.CharField(max_length=255, verbose_name="object ID")),
|
|
50
|
-
("id", models.BigAutoField(editable=False, primary_key=True, serialize=False, unique=True)),
|
|
51
|
-
("editable", models.BooleanField(default=True)),
|
|
52
|
-
("system", models.BooleanField(default=False)),
|
|
53
|
-
(
|
|
54
|
-
"content_type",
|
|
55
|
-
models.ForeignKey(
|
|
56
|
-
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
|
|
57
|
-
),
|
|
58
|
-
),
|
|
59
|
-
(
|
|
60
|
-
"permission",
|
|
61
|
-
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="auth.permission"),
|
|
62
|
-
),
|
|
63
|
-
(
|
|
64
|
-
"user",
|
|
65
|
-
models.ForeignKey(
|
|
66
|
-
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
|
67
|
-
),
|
|
68
|
-
),
|
|
69
|
-
],
|
|
70
|
-
options={
|
|
71
|
-
"db_table": "bridger_biguserobjectpermission",
|
|
72
|
-
"abstract": False,
|
|
73
|
-
"default_related_name": "userobjectpermissions",
|
|
74
|
-
"unique_together": {("user", "permission", "object_pk")},
|
|
75
|
-
},
|
|
76
|
-
),
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name='GroupObjectPermission',
|
|
21
|
+
fields=[
|
|
22
|
+
('object_pk', models.CharField(max_length=255, verbose_name='object ID')),
|
|
23
|
+
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False, unique=True)),
|
|
24
|
+
('editable', models.BooleanField(default=True)),
|
|
25
|
+
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
26
|
+
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
|
|
27
|
+
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
|
|
77
28
|
],
|
|
29
|
+
options={
|
|
30
|
+
'abstract': False,
|
|
31
|
+
'default_related_name': 'groupobjectpermissions',
|
|
32
|
+
'indexes': [models.Index(fields=['content_type', 'object_pk'], name='wbcore_perm_content_7e5298_idx'), models.Index(fields=['content_type', 'object_pk', 'group'], name='wbcore_perm_content_e0e7e3_idx')],
|
|
33
|
+
'unique_together': {('group', 'permission', 'object_pk')},
|
|
34
|
+
},
|
|
78
35
|
),
|
|
79
|
-
migrations.
|
|
80
|
-
name=
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
model_name="userobjectpermission",
|
|
97
|
-
index=models.Index(fields=["content_type", "object_pk"], name="wbcore_guar_content_138d49_idx"),
|
|
98
|
-
),
|
|
99
|
-
migrations.AddIndex(
|
|
100
|
-
model_name="userobjectpermission",
|
|
101
|
-
index=models.Index(fields=["content_type", "object_pk", "user"], name="wbcore_guar_content_98fc0f_idx"),
|
|
36
|
+
migrations.CreateModel(
|
|
37
|
+
name='UserObjectPermission',
|
|
38
|
+
fields=[
|
|
39
|
+
('object_pk', models.CharField(max_length=255, verbose_name='object ID')),
|
|
40
|
+
('id', models.BigAutoField(editable=False, primary_key=True, serialize=False, unique=True)),
|
|
41
|
+
('editable', models.BooleanField(default=True)),
|
|
42
|
+
('system', models.BooleanField(default=False)),
|
|
43
|
+
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
44
|
+
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
|
|
45
|
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
46
|
+
],
|
|
47
|
+
options={
|
|
48
|
+
'abstract': False,
|
|
49
|
+
'default_related_name': 'userobjectpermissions',
|
|
50
|
+
'indexes': [models.Index(fields=['content_type', 'object_pk'], name='wbcore_perm_content_753f13_idx'), models.Index(fields=['content_type', 'object_pk', 'user'], name='wbcore_perm_content_9efa9b_idx')],
|
|
51
|
+
'unique_together': {('user', 'permission', 'object_pk')},
|
|
52
|
+
},
|
|
102
53
|
),
|
|
103
54
|
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from celery import shared_task
|
|
2
|
+
from tqdm import tqdm
|
|
2
3
|
|
|
3
4
|
from wbcore.contrib.permission.models.mixins import PermissionObjectModelMixin
|
|
4
5
|
from wbcore.utils.itertools import get_inheriting_subclasses
|
|
@@ -6,7 +7,12 @@ from wbcore.workers import Queue
|
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
@shared_task(queue=Queue.EXTENDED_BACKGROUND.value)
|
|
9
|
-
def reload_permissions_as_task(
|
|
10
|
+
def reload_permissions_as_task(
|
|
11
|
+
prune_existing: bool | None = True, force_pruning: bool | None = False, debug: bool = False
|
|
12
|
+
):
|
|
10
13
|
for subclass in get_inheriting_subclasses(PermissionObjectModelMixin):
|
|
11
|
-
|
|
14
|
+
gen = subclass.objects.iterator()
|
|
15
|
+
if debug:
|
|
16
|
+
gen = tqdm(gen, total=subclass.objects.count())
|
|
17
|
+
for instance in gen:
|
|
12
18
|
instance.reload_permissions(prune_existing=prune_existing, force_pruning=force_pruning)
|
|
@@ -77,27 +77,43 @@ class DecimalField(NumberFieldMixin, WBCoreSerializerFieldMixin, serializers.Dec
|
|
|
77
77
|
|
|
78
78
|
return field_name, representation
|
|
79
79
|
|
|
80
|
+
def _is_inf(self, data) -> bool:
|
|
81
|
+
return isinstance(data, str) and data.lower().replace("-", "") == "infinity"
|
|
82
|
+
|
|
83
|
+
def validate_empty_values(self, data):
|
|
84
|
+
if self._is_inf(data):
|
|
85
|
+
data = None
|
|
86
|
+
return super().validate_empty_values(data)
|
|
87
|
+
|
|
88
|
+
def to_internal_value(self, data):
|
|
89
|
+
if self._is_inf(data):
|
|
90
|
+
return None
|
|
91
|
+
return super().to_internal_value(data)
|
|
92
|
+
|
|
80
93
|
|
|
81
94
|
class FloatField(NumberFieldMixin, WBCoreSerializerFieldMixin, serializers.FloatField):
|
|
82
95
|
field_type = WBCoreType.NUMBER.value
|
|
83
96
|
|
|
84
97
|
|
|
85
|
-
class DecimalRangeField(RangeMixin,
|
|
98
|
+
class DecimalRangeField(RangeMixin, DecimalField):
|
|
86
99
|
field_type = WBCoreType.NUMBERRANGE.value
|
|
87
100
|
internal_field = NumericRange
|
|
88
101
|
|
|
89
102
|
def to_representation(self, instance):
|
|
90
103
|
res = list(super().to_representation(instance))
|
|
91
104
|
# ensure empty value shows as None
|
|
92
|
-
if res[0]
|
|
105
|
+
if not res[0]:
|
|
93
106
|
res[0] = "-Infinity"
|
|
94
|
-
if res[1]
|
|
107
|
+
if not res[1]:
|
|
95
108
|
res[1] = "Infinity"
|
|
96
109
|
return tuple(res)
|
|
97
110
|
|
|
98
111
|
def __init__(self, max_digits=None, decimal_places=None, **kwargs):
|
|
99
112
|
if not max_digits:
|
|
100
|
-
max_digits =
|
|
113
|
+
max_digits = 12
|
|
101
114
|
if not decimal_places:
|
|
102
|
-
decimal_places =
|
|
115
|
+
decimal_places = 1
|
|
116
|
+
if "coerce_to_string" not in kwargs:
|
|
117
|
+
kwargs["coerce_to_string"] = True
|
|
118
|
+
kwargs["allow_null"] = True
|
|
103
119
|
super().__init__(max_digits, decimal_places, **kwargs)
|
|
@@ -116,7 +116,7 @@ wbcore/contrib/ai/llm/decorators.py,sha256=u3AWbQjbCCeIp-X1aezhqeZJd8chbI1OhGnm8
|
|
|
116
116
|
wbcore/contrib/ai/llm/mixins.py,sha256=qbuRLaRCmLjdMC7r877RKYsMpE6AurYEGEzSPYYE9y4,1037
|
|
117
117
|
wbcore/contrib/ai/llm/utils.py,sha256=B6iNt54x9ehN084wj-UvLUVEYrTPANSaITtl0AT3WJc,2503
|
|
118
118
|
wbcore/contrib/authentication/__init__.py,sha256=eQ3KJSYPoO4xc867RdcdayxaxZ8TLGLHMA3kY5H6UMg,313
|
|
119
|
-
wbcore/contrib/authentication/admin.py,sha256
|
|
119
|
+
wbcore/contrib/authentication/admin.py,sha256=NxUcMx4JPeCCQH-MBe_lIYXC4V3BHy5v_IW9YpulS9c,9702
|
|
120
120
|
wbcore/contrib/authentication/apps.py,sha256=5ak0rx5M2P7r4Mq1XnP4-arEHIUq6CAYoBjzOU7lPs4,411
|
|
121
121
|
wbcore/contrib/authentication/authentication.py,sha256=EMa-y7803EaH23voSREjMcfFXJr1tkAcbuwR5KCbL00,4295
|
|
122
122
|
wbcore/contrib/authentication/configs.py,sha256=Z3M2X6xzWhwJEEvHym6d9khriSdgyTmimbkxi7KAGuk,358
|
|
@@ -706,24 +706,23 @@ wbcore/contrib/io/tests/test_imports.py,sha256=0n99vPnItSeXMlwubr9g1k8UODR73eYQS
|
|
|
706
706
|
wbcore/contrib/io/tests/test_models.py,sha256=QJGHTs7pa1gqNcPyqqp2EIN7SwBbGp-kbIcL6lFmgBk,14232
|
|
707
707
|
wbcore/contrib/io/tests/test_viewsets.py,sha256=Fzd1G5gOc03gxpPwCX8juHgeazlSadZPq56k4ZvgoSY,9178
|
|
708
708
|
wbcore/contrib/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
709
|
-
wbcore/contrib/notifications/admin.py,sha256=
|
|
709
|
+
wbcore/contrib/notifications/admin.py,sha256=zHsqFg6-rRpYQ7J2Bc13iYpBTL3gVS-ynukAaCqZ89I,1649
|
|
710
710
|
wbcore/contrib/notifications/apps.py,sha256=O5rwHeCn3FBTE1rjJgNDahJcGXQqezvIEX7T1RtBsZ8,1883
|
|
711
711
|
wbcore/contrib/notifications/configs.py,sha256=Brt_79I8teg-sLzy6KBVsb0eXg-KFmgv_t-bNdznX5k,538
|
|
712
712
|
wbcore/contrib/notifications/configurations.py,sha256=ZoNd8cdbw8Y-rQIQtS2VgieUO8FQ-kdgZdzNMwqL484,327
|
|
713
|
-
wbcore/contrib/notifications/dispatch.py,sha256=
|
|
714
|
-
wbcore/contrib/notifications/tasks.py,sha256=rdOFPLPiRKEuP9YqAdhnQD-93GvR4C0JykwCD9ziF7w,2117
|
|
713
|
+
wbcore/contrib/notifications/dispatch.py,sha256=QT506c6rJWnuWdPqhJAqAf_ABhY21kJQ9RDcimqqJ5g,3143
|
|
715
714
|
wbcore/contrib/notifications/urls.py,sha256=C764DdlQcJaPjzRKqKrLxKMhpZITZe-YLZaAQOFtzys,872
|
|
716
|
-
wbcore/contrib/notifications/utils.py,sha256=
|
|
715
|
+
wbcore/contrib/notifications/utils.py,sha256=lk-afWTS-8LMYWFmGwx0mvbRCgFiY9DNF9i1uOq91YQ,1194
|
|
717
716
|
wbcore/contrib/notifications/views.py,sha256=kr7UNwSdQpiwc0-N2KB6zsqNXcodsAcu2wpPblHZoO0,2601
|
|
718
717
|
wbcore/contrib/notifications/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
719
718
|
wbcore/contrib/notifications/backends/abstract_backend.py,sha256=gdCQn3YRoTGOy19N3TNfsBOmNbqDWZgeBRSmAEhCM2c,304
|
|
720
719
|
wbcore/contrib/notifications/backends/console/__init__.py,sha256=vgrN2iIcVZfFSRJ2A5ltDQYOCK0DCAkt79JU4ZSfwTQ,42
|
|
721
720
|
wbcore/contrib/notifications/backends/console/backends.py,sha256=3oCasfFwlSvW6KMFWgI6mTTwTS8e60Xi8Kb831hp4tk,765
|
|
722
721
|
wbcore/contrib/notifications/backends/firebase/__init__.py,sha256=vgrN2iIcVZfFSRJ2A5ltDQYOCK0DCAkt79JU4ZSfwTQ,42
|
|
723
|
-
wbcore/contrib/notifications/backends/firebase/backends.py,sha256=
|
|
722
|
+
wbcore/contrib/notifications/backends/firebase/backends.py,sha256=1uq3Gh9810CfaUBAhuWEBZEn1ISEdMa1FbabqoJlayw,4222
|
|
724
723
|
wbcore/contrib/notifications/factories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
725
724
|
wbcore/contrib/notifications/factories/notification_types.py,sha256=s2R4M6FSeIt36E8Zc6Zg9KFUS3Kg1r6G5uEzy2IbhMM,706
|
|
726
|
-
wbcore/contrib/notifications/factories/notifications.py,sha256=
|
|
725
|
+
wbcore/contrib/notifications/factories/notifications.py,sha256=QSZr6zoKrlic69gqdBikwbnuz5ypgwm2j1pBL29aqJ4,542
|
|
727
726
|
wbcore/contrib/notifications/factories/tokens.py,sha256=_L82yARrWkTEManCJzu3HLmUprXcsYLA471Om1FRcpo,458
|
|
728
727
|
wbcore/contrib/notifications/locale/de/LC_MESSAGES/django.po,sha256=d5gqNmW90TvbN78pxgUJI9Ep3adTAffgVA3SWOfX8N8,2102
|
|
729
728
|
wbcore/contrib/notifications/locale/de/LC_MESSAGES/django.po.translated,sha256=U_ybUKrzfqWsy6bEb1SwFImXRT7w2GMVE9bvhIdGqKY,2088
|
|
@@ -738,10 +737,11 @@ wbcore/contrib/notifications/migrations/0006_notification_created.py,sha256=2trD
|
|
|
738
737
|
wbcore/contrib/notifications/migrations/0007_notificationtype_resource_button_label.py,sha256=oSVy8y9uQMrw2BGCNKy6WhaeMali69yuHGgfX1HXyyw,480
|
|
739
738
|
wbcore/contrib/notifications/migrations/0008_notificationtype_is_lock.py,sha256=7uMgkxo7AcLQrvTR2oiYyrCpAcY7NZnp9vcd1rmi7sc,425
|
|
740
739
|
wbcore/contrib/notifications/migrations/0009_alter_notificationtypesetting_options_and_more.py,sha256=yjkAbfbZMHjJ_DLBHXg792AYzeOlLKyp-sAuVt-IpNI,1311
|
|
740
|
+
wbcore/contrib/notifications/migrations/0010_notification_checksum_notification_checksum_unique.py,sha256=lLZml8heSjFossj-fzaJ9yUMHdLyDReOj3P-A8Xq4-w,1724
|
|
741
741
|
wbcore/contrib/notifications/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
742
742
|
wbcore/contrib/notifications/models/__init__.py,sha256=l7E4YjFreG6YeBCjcYJrK1Zn2vfPiBpF6bbfIb_5VM4,156
|
|
743
|
-
wbcore/contrib/notifications/models/notification_types.py,sha256=
|
|
744
|
-
wbcore/contrib/notifications/models/notifications.py,sha256=
|
|
743
|
+
wbcore/contrib/notifications/models/notification_types.py,sha256=vujSAprxaZDGhTqLXmKRPXGuIrLg50Fs1w5iBVQxJjE,5819
|
|
744
|
+
wbcore/contrib/notifications/models/notifications.py,sha256=yGR82Br-EFUzaD5umyV6UAI-Szl0qIld56Osi-7cmeA,5942
|
|
745
745
|
wbcore/contrib/notifications/models/tokens.py,sha256=agFhkmCwBNmK9TM1ylMSonXRd1h782sJqoms58z5ucg,1545
|
|
746
746
|
wbcore/contrib/notifications/release_notes/1_0_0.md,sha256=pyuuGK8zEp7sbPchnpXyPngwJ9-qvrm0dJh3P2DJGIg,168
|
|
747
747
|
wbcore/contrib/notifications/release_notes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -751,22 +751,21 @@ wbcore/contrib/notifications/serializers/notifications.py,sha256=tNMIIYzSJYwmxF2
|
|
|
751
751
|
wbcore/contrib/notifications/static/notifications/service-worker.js,sha256=4_yl6qd0ituPK5KxFIyYXs6zXOIc_Y4wtzXKzWkSnqw,4926
|
|
752
752
|
wbcore/contrib/notifications/templates/notifications/notification_template.html,sha256=k-o9ieU6z6c0SKAw43_iLEleJTIZK43Y8jrRCwbKPCc,1441
|
|
753
753
|
wbcore/contrib/notifications/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
754
|
-
wbcore/contrib/notifications/tests/conftest.py,sha256=
|
|
754
|
+
wbcore/contrib/notifications/tests/conftest.py,sha256=H1by5FovsOOTE-a4TsovtJsUrv57KLTM6WHOTqq5frs,1455
|
|
755
755
|
wbcore/contrib/notifications/tests/test_configs.py,sha256=2V3m8EurnV7FlnVpexawfgR5fsOUSSe9RDZMzWqWowk,283
|
|
756
|
-
wbcore/contrib/notifications/tests/test_tasks.py,sha256=d8EtXHNPZFU_JwTiVDqpdjBsxbZuRZ-NruEuaqt306M,2338
|
|
757
756
|
wbcore/contrib/notifications/tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
758
757
|
wbcore/contrib/notifications/tests/test_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
759
758
|
wbcore/contrib/notifications/tests/test_backends/test_firebase.py,sha256=RoC9fbj5OWhW_jn2_ZHVMG-t2aYYl3IKH98fCHXFZW8,3804
|
|
760
759
|
wbcore/contrib/notifications/tests/test_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
761
760
|
wbcore/contrib/notifications/tests/test_models/test_notification_types.py,sha256=t4Cozw2hgDoGc9uC5elX7q2kK5UWY-l4bMU4Fo6L2fs,3428
|
|
762
|
-
wbcore/contrib/notifications/tests/test_models/test_notifications.py,sha256=
|
|
761
|
+
wbcore/contrib/notifications/tests/test_models/test_notifications.py,sha256=bF4PhEw4olAo_OiLqoupbVYBE2bd-LJVcOgoavB1DDg,6250
|
|
763
762
|
wbcore/contrib/notifications/tests/test_models/test_tokens.py,sha256=8U9WBmHBLMZcNig_doDdByIFYH2V4ccYwnPsgpw9bIc,1301
|
|
764
763
|
wbcore/contrib/notifications/tests/test_serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
765
764
|
wbcore/contrib/notifications/tests/test_serializers/test_notification_types.py,sha256=TIiMdwaXWXRSzeFNKenMAR7VT0fX_HNdxnO9nsTRr6w,2536
|
|
766
765
|
wbcore/contrib/notifications/tests/test_serializers/test_notifications.py,sha256=kW4uTL8Vr_gwo-zAZv4q_v3BBTAmyUDvUv0SzODB2YI,940
|
|
767
766
|
wbcore/contrib/notifications/tests/test_viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
768
767
|
wbcore/contrib/notifications/tests/test_viewsets/test_notification_types.py,sha256=GSmHRc1tUn5Z6M872ZTzaKvgzh7ManfLdYv2BkjN7oc,5851
|
|
769
|
-
wbcore/contrib/notifications/tests/test_viewsets/test_notifications.py,sha256=
|
|
768
|
+
wbcore/contrib/notifications/tests/test_viewsets/test_notifications.py,sha256=YNSUUcGgCq_LSRqboGL3o-8Vd4ZqxKzinyYeuo6GmIY,6428
|
|
770
769
|
wbcore/contrib/notifications/viewsets/__init__.py,sha256=LvuSAnXRTv5tixtnLaLnx1OFomQaKumL5fo_1EtfBzw,172
|
|
771
770
|
wbcore/contrib/notifications/viewsets/menus.py,sha256=NvhqCjGwpq21DRhvNJKCcv2MAvHIyxq38JLrsV3zR8s,534
|
|
772
771
|
wbcore/contrib/notifications/viewsets/notification_types.py,sha256=FrOxSiQzuO6zC6vQl-C4UZBjvf6J-xyZkCGzueKA_hk,1900
|
|
@@ -792,14 +791,14 @@ wbcore/contrib/permission/apps.py,sha256=ukDa4Mox01a0gtS5t-FmdPOeXTj_dF-lsXe0Gbj
|
|
|
792
791
|
wbcore/contrib/permission/configurations.py,sha256=bqg-soAJKU70vVuAuwhnV5Dzfa1XkJ1TyuZaXhMfAr4,187
|
|
793
792
|
wbcore/contrib/permission/filters.py,sha256=Jt9HuDpCPITGJQ0aWyG3HxmNM8MVsqV-FfRy_-g5ebs,1069
|
|
794
793
|
wbcore/contrib/permission/permissions.py,sha256=1fHUA9o0u02Zhzk2zmFvtBqJgtlVe-R7AkIA0hUgbfk,1777
|
|
795
|
-
wbcore/contrib/permission/tasks.py,sha256=
|
|
794
|
+
wbcore/contrib/permission/tasks.py,sha256=WUiy4Fn1kwWeNoM7c6Bfz7_TOPmmx7xU8Zuz8kdeHRM,736
|
|
796
795
|
wbcore/contrib/permission/urls.py,sha256=rzkEtFSJNluVgM-r3KCxnqtSYzCAy0OGKMXFpcLg74I,415
|
|
797
796
|
wbcore/contrib/permission/utils.py,sha256=d64UjkcMfqT5YLiTjhTUFRWpDTu6sVat-1nx6XcNaRg,6110
|
|
798
797
|
wbcore/contrib/permission/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
799
798
|
wbcore/contrib/permission/internal/backend.py,sha256=pLK_Hpk_2O4pMXEnUkw_dDYG2Jagm4vK0ER9eZt80MY,1368
|
|
800
799
|
wbcore/contrib/permission/internal/registry.py,sha256=e6CrXNP5Wx-sDUerotM1sMAH2_lzhDqsfrW72w0XQZM,1571
|
|
801
800
|
wbcore/contrib/permission/management/__init__.py,sha256=AdparB3Q-UQ_wYKXXXuKK6PozIg9x3CowQIkmY0qSHE,164
|
|
802
|
-
wbcore/contrib/permission/migrations/0001_initial.py,sha256=
|
|
801
|
+
wbcore/contrib/permission/migrations/0001_initial.py,sha256=8i15T3jYDsSFqISwCQF0IaUsoFRghfvRkhmVolcp2Nk,2875
|
|
803
802
|
wbcore/contrib/permission/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
804
803
|
wbcore/contrib/permission/models/__init__.py,sha256=63_HSsJbwNgPPt67vyZKHm5dqB8X-YIAF6tRlK9YgQk,64
|
|
805
804
|
wbcore/contrib/permission/models/mixins.py,sha256=0VTVYQT3mOeGt0HYDtyTVEH5TBCoB-lNwcHKewjboos,7422
|
|
@@ -1113,7 +1112,7 @@ wbcore/serializers/fields/fsm.py,sha256=xUYxDj166PDnmDLggI4fShXdSunJVzbc8quFQioM
|
|
|
1113
1112
|
wbcore/serializers/fields/json.py,sha256=8SmEOW2hXnTTfuCztaxA8AA3qtTxhCZtft7BJm1yO6o,2225
|
|
1114
1113
|
wbcore/serializers/fields/list.py,sha256=cYmMBg9M10Qb_5z98CeP3SjE2bV-u7Z0xkzXr-C3xoA,4178
|
|
1115
1114
|
wbcore/serializers/fields/mixins.py,sha256=sP1mEiD5tMZ4yCr-X0IkIY3RZwJcT0JokvontvRsDKw,7613
|
|
1116
|
-
wbcore/serializers/fields/number.py,sha256=
|
|
1115
|
+
wbcore/serializers/fields/number.py,sha256=4L8Hjf_BWzX3M-ZWKW-D7XzAedy5fXr1gJnPcZ4gVno,4256
|
|
1117
1116
|
wbcore/serializers/fields/other.py,sha256=3r_70JH_A_daS99LuwQWwa0LNtyosKW7QKJzZgQA-zo,1131
|
|
1118
1117
|
wbcore/serializers/fields/primary_key.py,sha256=yTbs5B2QlUX-XKEtop3JpwIPeP-FhM8u-2qDXM5q6u0,676
|
|
1119
1118
|
wbcore/serializers/fields/related.py,sha256=mq7QhcjSG273G400ZueYJnNVNDlGgnUHLoAHaKRjW_Q,6355
|
|
@@ -1237,6 +1236,6 @@ wbcore/viewsets/generics.py,sha256=lKDq9UY_Tyc56u1bqaIEvHGgoaXwXxpZ1c3fLVteptI,1
|
|
|
1237
1236
|
wbcore/viewsets/mixins.py,sha256=KJxbd2arSplZg2dMD37VgDhCDQa69PyxtOxiCir0eXg,12059
|
|
1238
1237
|
wbcore/viewsets/utils.py,sha256=4520Ij3ASM8lOa8QZkCqbBfOexVRiZu688eW-PGqMOA,882
|
|
1239
1238
|
wbcore/viewsets/viewsets.py,sha256=G6BVJ3NzieT0TaeC1T3v8oKIT-nm61INLXnTus_6LUQ,6588
|
|
1240
|
-
wbcore-1.61.
|
|
1241
|
-
wbcore-1.61.
|
|
1242
|
-
wbcore-1.61.
|
|
1239
|
+
wbcore-1.61.6.dist-info/METADATA,sha256=xFMBS18bxKaJxpbOOEZTpJu5b96DpUTjj3wuyYjZyp0,2316
|
|
1240
|
+
wbcore-1.61.6.dist-info/WHEEL,sha256=aha0VrrYvgDJ3Xxl3db_g_MDIW-ZexDdrc_m-Hk8YY4,105
|
|
1241
|
+
wbcore-1.61.6.dist-info/RECORD,,
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
from celery import shared_task
|
|
2
|
-
from django.conf import settings
|
|
3
|
-
from django.core.mail.message import EmailMultiAlternatives
|
|
4
|
-
from django.template.loader import get_template
|
|
5
|
-
from django.utils.html import strip_tags
|
|
6
|
-
from django.utils.module_loading import import_string
|
|
7
|
-
|
|
8
|
-
from wbcore.contrib.notifications.models.notification_types import (
|
|
9
|
-
NotificationTypeSetting,
|
|
10
|
-
)
|
|
11
|
-
from wbcore.contrib.notifications.models.notifications import Notification
|
|
12
|
-
from wbcore.workers import Queue
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def send_notification_email(notification: Notification):
|
|
16
|
-
"""Sends out a notification to the user specified inside the notification
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
notification: The notification that is going to be send
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
context = {
|
|
23
|
-
"title": notification.title,
|
|
24
|
-
"message": notification.body or "",
|
|
25
|
-
"notification_share_url": notification.get_full_endpoint(as_shareable_internal_link=True),
|
|
26
|
-
"notification_endpoint": notification.get_full_endpoint(),
|
|
27
|
-
}
|
|
28
|
-
rendered_template = get_template("notifications/notification_template.html").render(context)
|
|
29
|
-
msg = EmailMultiAlternatives(
|
|
30
|
-
subject=notification.title,
|
|
31
|
-
body=strip_tags(rendered_template),
|
|
32
|
-
from_email=getattr(settings, "WBCORE_NOTIFICATION_EMAIL_FROM", "no_reply@stainly.com"),
|
|
33
|
-
to=[notification.user.email], # type: ignore
|
|
34
|
-
)
|
|
35
|
-
msg.attach_alternative(rendered_template, "text/html")
|
|
36
|
-
msg.send()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@shared_task(queue=Queue.HIGH_PRIORITY.value)
|
|
40
|
-
def send_notification_task(notification_pk: int):
|
|
41
|
-
"""A celery task to send out a notification via email, web or mobile
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
notification_pk: The primary key of the notification that is going to be send out
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
notification = Notification.objects.get(pk=notification_pk)
|
|
48
|
-
|
|
49
|
-
notification_user_settings = NotificationTypeSetting.objects.get(
|
|
50
|
-
notification_type=notification.notification_type,
|
|
51
|
-
user=notification.user,
|
|
52
|
-
)
|
|
53
|
-
if notification_user_settings.enable_email:
|
|
54
|
-
send_notification_email(notification)
|
|
55
|
-
|
|
56
|
-
backend = import_string(settings.NOTIFICATION_BACKEND)
|
|
57
|
-
backend.send_notification(notification)
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from bs4 import BeautifulSoup
|
|
3
|
-
from django.core import mail
|
|
4
|
-
from django.test.utils import override_settings
|
|
5
|
-
|
|
6
|
-
from wbcore.contrib.notifications import tasks
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@pytest.fixture
|
|
10
|
-
def patched_send_notification_email(mocker):
|
|
11
|
-
return mocker.patch("wbcore.contrib.notifications.tasks.send_notification_email")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@pytest.fixture
|
|
15
|
-
def patched_import_string(mocker):
|
|
16
|
-
return mocker.patch("wbcore.contrib.notifications.tasks.import_string")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@pytest.fixture
|
|
20
|
-
def mock_backend():
|
|
21
|
-
from wbcore.contrib.notifications.backends.abstract_backend import (
|
|
22
|
-
AbstractNotificationBackend,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
class MockedBackend(AbstractNotificationBackend):
|
|
26
|
-
@classmethod
|
|
27
|
-
def send_notification(cls, notification):
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
@classmethod
|
|
31
|
-
def get_configuration(cls) -> dict:
|
|
32
|
-
return {}
|
|
33
|
-
|
|
34
|
-
return MockedBackend
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@pytest.mark.django_db
|
|
38
|
-
def test_send_notification_task(
|
|
39
|
-
notification, mock_backend, mocker, patched_send_notification_email, patched_import_string
|
|
40
|
-
):
|
|
41
|
-
notification.user.wbnotification_user_settings.update(enable_email=True)
|
|
42
|
-
spy = mocker.spy(mock_backend, "send_notification")
|
|
43
|
-
patched_import_string.return_value = mock_backend
|
|
44
|
-
|
|
45
|
-
tasks.send_notification_task(notification.pk)
|
|
46
|
-
|
|
47
|
-
patched_send_notification_email.assert_called_once_with(notification)
|
|
48
|
-
patched_import_string.assert_called_once()
|
|
49
|
-
spy.assert_called_once_with(notification)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@pytest.mark.django_db
|
|
53
|
-
def test_send_notification_task_without_mail(
|
|
54
|
-
notification, mock_backend, patched_send_notification_email, patched_import_string
|
|
55
|
-
):
|
|
56
|
-
notification.user.wbnotification_user_settings.update(enable_email=False)
|
|
57
|
-
patched_import_string.return_value = mock_backend
|
|
58
|
-
|
|
59
|
-
tasks.send_notification_task(notification.pk)
|
|
60
|
-
|
|
61
|
-
assert not patched_send_notification_email.called
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@pytest.mark.django_db
|
|
65
|
-
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
|
66
|
-
@pytest.mark.parametrize("notification__endpoint", ["/wbcore/notifications/"])
|
|
67
|
-
def test_send_notification_email(notification):
|
|
68
|
-
assert len(mail.outbox) == 0
|
|
69
|
-
tasks.send_notification_email(notification)
|
|
70
|
-
assert len(mail.outbox) == 1
|
|
71
|
-
soup = BeautifulSoup(mail.outbox[0].alternatives[0][0], "html.parser")
|
|
72
|
-
assert soup.find("a", href=notification.get_full_endpoint(as_shareable_internal_link=True))
|
|
File without changes
|