Skip to content

Commit d84235d

Browse files
committed
feat: Added new NotificationType.forbidden_channels property to disable certain channels for certain notification types
1 parent da573b2 commit d84235d

File tree

8 files changed

+121
-10
lines changed

8 files changed

+121
-10
lines changed

example/notifications/templates/settings.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ <h1 class="text-xl">Notification settings</h1>
4141
name="{{ type_data.notification_type.key }}__{{ channel_key }}"
4242
class="checkbox checkbox-primary"
4343
{% if channel_data.enabled %}checked{% endif %}
44-
{% if channel_data.required %}disabled{% endif %}
45-
{% if channel_key == 'email' %}@change="emailEnabled = $event.target.checked"{% endif %}
46-
{% if channel_data.required %}title="Required"{% endif %} />
44+
{% if channel_data.forbidden %}disabled title="Forbidden"{% endif %}
45+
{% if channel_data.required %}disabled title="Required"{% endif %}
46+
{% if channel_key == 'email' %}@change="emailEnabled = $event.target.checked"{% endif %} />
4747
{% endwith %}
4848
</td>
4949
{% endfor %}

example/notifications/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from generic_notifications.channels import EmailChannel
12
from generic_notifications.types import NotificationType, register
23

34

@@ -6,3 +7,11 @@ class CommentNotificationType(NotificationType):
67
key = "comment_notification"
78
name = "Comments"
89
description = "You received a comment"
10+
11+
12+
@register
13+
class WebsiteOnlyNotificationType(NotificationType):
14+
key = "website_only_notification"
15+
name = "Website only"
16+
description = "Just a test notification"
17+
forbidden_channels = [EmailChannel]

example/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

generic_notifications/preferences.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ def get_notification_preferences(user: "AbstractUser") -> List[Dict[str, Any]]:
4545
channel_key = channel.key
4646
is_disabled = (type_key, channel_key) in disabled_channels
4747
is_required = channel_key in [ch.key for ch in notification_type.required_channels]
48+
is_forbidden = channel_key in [ch.key for ch in notification_type.forbidden_channels]
4849

4950
type_data["channels"][channel_key] = {
5051
"channel": channel,
51-
"enabled": is_required or not is_disabled, # Required channels are always enabled
52+
"enabled": not is_forbidden and (is_required or not is_disabled),
5253
"required": is_required,
54+
"forbidden": is_forbidden,
5355
}
5456

5557
settings_data.append(type_data)
@@ -85,9 +87,11 @@ def save_notification_preferences(user: "AbstractUser", form_data: Dict[str, Any
8587
channel_key = channel.key
8688
form_key = f"{type_key}__{channel_key}"
8789

88-
# Check if this channel is required (cannot be disabled)
90+
# Check if this channel is required or forbidden (cannot be changed)
8991
if channel_key in [ch.key for ch in notification_type.required_channels]:
9092
continue
93+
if channel_key in [ch.key for ch in notification_type.forbidden_channels]:
94+
continue
9195

9296
# If checkbox not checked, create disabled entry
9397
if form_key not in form_data:

generic_notifications/types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class NotificationType(ABC):
1919
description: str
2020
default_frequency: Type[BaseFrequency] = DailyFrequency
2121
required_channels: list[Type[BaseChannel]] = []
22+
forbidden_channels: list[Type[BaseChannel]] = []
2223

2324
def __str__(self) -> str:
2425
return self.name
@@ -117,10 +118,13 @@ def get_enabled_channels(cls, user: Any) -> list[Type[BaseChannel]]:
117118
)
118119
)
119120

120-
# Filter out disabled channels
121+
# Get all forbidden channel keys
122+
forbidden_channel_keys = {channel_cls.key for channel_cls in cls.forbidden_channels}
123+
124+
# Filter out disabled and forbidden channels
121125
enabled_channels = []
122126
for channel_cls in registry.get_all_channels():
123-
if channel_cls.key not in disabled_channel_keys:
127+
if channel_cls.key not in disabled_channel_keys and channel_cls.key not in forbidden_channel_keys:
124128
enabled_channels.append(channel_cls)
125129

126130
return enabled_channels

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "django-generic-notifications"
3-
version = "2.0.1"
3+
version = "2.1.0"
44
description = "A flexible, multi-channel notification system for Django applications with built-in support for email digests, user preferences, and extensible delivery channels."
55
authors = [
66
{name = "Kevin Renskers", email = "[email protected]"},

tests/test_types.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,56 @@ def test_reset_to_default(self):
213213
# Getting frequency should now return the default
214214
frequency_cls = TestNotificationType.get_frequency(self.user)
215215
self.assertEqual(frequency_cls, TestNotificationType.default_frequency)
216+
217+
218+
class ForbiddenChannelsNotificationType(NotificationType):
219+
key = "forbidden_test_type"
220+
name = "Forbidden Test Type"
221+
description = "A test notification type with forbidden channels"
222+
forbidden_channels = [WebsiteChannel]
223+
224+
225+
class TestForbiddenChannels(TestCase):
226+
user: Any
227+
228+
@classmethod
229+
def setUpClass(cls):
230+
super().setUpClass()
231+
cls.user = User.objects.create_user(
232+
username="forbidden_test", email="[email protected]", password="testpass"
233+
)
234+
registry.register_type(ForbiddenChannelsNotificationType)
235+
236+
def test_forbidden_channels_not_in_enabled_channels(self):
237+
"""Test that forbidden channels are not included in get_enabled_channels"""
238+
enabled_channels = ForbiddenChannelsNotificationType.get_enabled_channels(self.user)
239+
enabled_channel_keys = [ch.key for ch in enabled_channels]
240+
241+
# Website channel should not be in enabled channels
242+
self.assertNotIn(WebsiteChannel.key, enabled_channel_keys)
243+
# Email channel should still be enabled
244+
self.assertIn(EmailChannel.key, enabled_channel_keys)
245+
246+
def test_forbidden_channels_filtered_even_when_explicitly_enabled(self):
247+
"""Test that forbidden channels are filtered out even if user tries to enable them"""
248+
# Try to enable the forbidden channel (this should have no effect)
249+
ForbiddenChannelsNotificationType.enable_channel(self.user, WebsiteChannel)
250+
251+
enabled_channels = ForbiddenChannelsNotificationType.get_enabled_channels(self.user)
252+
enabled_channel_keys = [ch.key for ch in enabled_channels]
253+
254+
# Website channel should still not be in enabled channels
255+
self.assertNotIn(WebsiteChannel.key, enabled_channel_keys)
256+
257+
def test_forbidden_channels_filtered_when_not_disabled(self):
258+
"""Test that forbidden channels are filtered out regardless of disabled state"""
259+
# Ensure no disabled entry exists for the forbidden channel
260+
DisabledNotificationTypeChannel.objects.filter(
261+
user=self.user, notification_type=ForbiddenChannelsNotificationType.key, channel=WebsiteChannel.key
262+
).delete()
263+
264+
enabled_channels = ForbiddenChannelsNotificationType.get_enabled_channels(self.user)
265+
enabled_channel_keys = [ch.key for ch in enabled_channels]
266+
267+
# Website channel should still not be in enabled channels
268+
self.assertNotIn(WebsiteChannel.key, enabled_channel_keys)

tests/test_utils.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.test import TestCase
55

66
from generic_notifications import send_notification
7-
from generic_notifications.channels import WebsiteChannel
7+
from generic_notifications.channels import EmailChannel, WebsiteChannel
88
from generic_notifications.models import Notification
99
from generic_notifications.registry import registry
1010
from generic_notifications.types import NotificationType
@@ -122,6 +122,47 @@ def test_send_notification_multiple_channels(self):
122122
self.assertIn("email", channel_keys)
123123
self.assertEqual(len(channel_keys), 2)
124124

125+
def test_send_notification_with_disabled_channel(self):
126+
# Disable website channel for this user
127+
self.notification_type.disable_channel(self.user, WebsiteChannel)
128+
129+
notification = send_notification(recipient=self.user, notification_type=self.notification_type)
130+
131+
# Notification should only have email channel
132+
self.assertIsNotNone(notification)
133+
channel_keys = notification.get_channels()
134+
self.assertNotIn("website", channel_keys)
135+
self.assertIn("email", channel_keys)
136+
self.assertEqual(len(channel_keys), 1)
137+
138+
def test_send_notification_with_all_channels_disabled(self):
139+
# Disable both channels for this user
140+
self.notification_type.disable_channel(self.user, WebsiteChannel)
141+
self.notification_type.disable_channel(self.user, EmailChannel)
142+
143+
notification = send_notification(recipient=self.user, notification_type=self.notification_type)
144+
145+
# Should return None when no channels are enabled
146+
self.assertIsNone(notification)
147+
148+
def test_send_notification_with_forbidden_channels(self):
149+
# Create a notification type with forbidden channels
150+
class ForbiddenTestType(NotificationType):
151+
key = "forbidden_test"
152+
name = "Forbidden Test"
153+
forbidden_channels = [WebsiteChannel]
154+
155+
registry.register_type(ForbiddenTestType)
156+
157+
notification = send_notification(recipient=self.user, notification_type=ForbiddenTestType)
158+
159+
# Should only have email channel (website is forbidden)
160+
self.assertIsNotNone(notification)
161+
channel_keys = notification.get_channels()
162+
self.assertNotIn("website", channel_keys)
163+
self.assertIn("email", channel_keys)
164+
self.assertEqual(len(channel_keys), 1)
165+
125166

126167
class MarkNotificationsAsReadTest(TestCase):
127168
def setUp(self):

0 commit comments

Comments
 (0)