wbcore 1.61.5__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.
@@ -78,7 +78,13 @@ class UserAdmin(admin.ModelAdmin):
78
78
  (_("Synchronization"), {"fields": ("metadata",)}),
79
79
  (
80
80
  _("Permissions"),
81
- {"fields": ("is_active", "is_register", "is_staff", "is_superuser", "groups", "user_permissions")},
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.tasks import send_notification_task
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
- send_notification_task.delay(notification.id)
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
- notification_user_settings = NotificationTypeSetting.objects.get(
38
- notification_type=notification.notification_type,
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 Notification, NotificationType, NotificationTypeSetting
17
- from .tasks import send_notification_task
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 = user
43
- if isinstance(users, User):
44
- users = [users]
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: send_notification_task.delay(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 NotificationModelFactory(factory.django.DjangoModelFactory):
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)
@@ -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
- try:
116
- existing_setting = NotificationTypeSetting.objects.get(user=user, notification_type=instance)
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
- objs.append(
154
- NotificationTypeSetting(
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(null=True, blank=True)
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
- NotificationModelFactory,
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(NotificationModelFactory, name="notification")
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
- NotificationModelFactory,
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 = NotificationModelFactory()
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
- NotificationModelFactory(user=user, read=timezone.now())
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
- NotificationModelFactory(user=user2, read=timezone.now())
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
 
@@ -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, WBCoreSerializerFieldMixin, serializers.DecimalField):
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] is None:
105
+ if not res[0]:
93
106
  res[0] = "-Infinity"
94
- if res[1] is None:
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 = 3
113
+ max_digits = 12
101
114
  if not decimal_places:
102
- decimal_places = 2
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbcore
3
- Version: 1.61.5
3
+ Version: 1.61.6
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  Requires-Dist: celery[redis]==5.*
6
6
  Requires-Dist: croniter==2.*
@@ -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=-lhV1R36bYFYPMMuYncSwVs4Xl-qk8_Jd6WCdrLrhnk,9570
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=aeLRLXw-5-fRXrrRAyFBtoFo8rzu8XSozM1_q-yWdV8,1672
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=6ioFv00hf_5wl7G_4-yWAhZ6P2HLcfvA8cpmzwijPHs,3308
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=Ou8JwrsDfJ_OHaz4qJuNrR5nhgKXAatRyFyWwAYrCaY,929
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=cPV_V0ikgMP30JtRTB7ZYf1Bv__3KZcOqOrqhyRfinc,4398
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=QOO0Wp0x2Ij3YZQS6LlWMawbWtiy8yfUBk8vK6NWGok,547
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=nlL7vTKmflrOr1Cp_Wf9-sN-UyoalzSQd-1tyjyC5rI,6007
744
- wbcore/contrib/notifications/models/notifications.py,sha256=DP76m7Z4Zcqb4NGc1k7D13m-gB63R0x9ktrZhLvwLWw,2697
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=QthEmk6WnvMsvo9Rdva7RGGT8grNlGvSLdD9Ggj4480,1486
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=N98NHI-9LvZi2YA1ruawc6NEfS06iO4fQrlux1PS50I,2056
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=cuY6k_LJfA4AHLLXzm9bYhjvuCqS-4VKOByUVUjlDTw,6448
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
@@ -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=qdPxELB6Up_BfA_cl_ouqmCKZha8JE0RNMn0b3w19Ls,3761
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.5.dist-info/METADATA,sha256=yBwr3aJOjETnSWX9A7V1rSs-5QysKQJuj710aA1Ula8,2316
1241
- wbcore-1.61.5.dist-info/WHEEL,sha256=aha0VrrYvgDJ3Xxl3db_g_MDIW-ZexDdrc_m-Hk8YY4,105
1242
- wbcore-1.61.5.dist-info/RECORD,,
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))