Skip to content

Commit 43d76c4

Browse files
feat: Added helper methods NotificationType (#4)
Added helper methods to NotificationType: - set_email_frequency - get_email_frequency - reset_email_frequency_to_default - get_enabled_channels - is_channel_enabled - disable_channel - enable_channel Also internally everything now uses types rather than instances, and no more string arguments either.
1 parent a57f05d commit 43d76c4

File tree

13 files changed

+482
-195
lines changed

13 files changed

+482
-195
lines changed

README.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,10 @@ from generic_notifications.frequencies import RealtimeFrequency
116116
from myapp.notifications import CommentNotification
117117

118118
# Disable email channel for comment notifications
119-
DisabledNotificationTypeChannel.objects.create(
120-
user=user,
121-
notification_type=CommentNotification.key,
122-
channel=EmailChannel.key
123-
)
119+
CommentNotification.disable_channel(user=user, channel=EmailChannel)
124120

125121
# Change to realtime digest for a notification type
126-
EmailFrequency.objects.update_or_create(
127-
user=user,
128-
notification_type=CommentNotification.key,
129-
defaults={'frequency': RealtimeFrequency.key}
130-
)
122+
CommentNotification.set_email_frequency(user=user, frequency=RealtimeFrequency)
131123
```
132124

133125
This project doesn't come with a UI (view + template) for managing user preferences, but an example is provided in the [example app](#example-app).

generic_notifications/__init__.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,10 @@ def send_notification(
5656
)
5757

5858
# Determine which channels are enabled for this user/notification type
59-
enabled_channels = []
60-
enabled_channel_instances = []
61-
for channel_instance in registry.get_all_channels():
62-
if channel_instance.is_enabled(recipient, notification_type.key):
63-
enabled_channels.append(channel_instance.key)
64-
enabled_channel_instances.append(channel_instance)
59+
enabled_channel_classes = notification_type.get_enabled_channels(recipient)
6560

6661
# Don't create notification if no channels are enabled
67-
if not enabled_channels:
62+
if not enabled_channel_classes:
6863
return None
6964

7065
# Create the notification record with enabled channels
@@ -73,7 +68,7 @@ def send_notification(
7368
notification_type=notification_type.key,
7469
actor=actor,
7570
target=target,
76-
channels=enabled_channels,
71+
channels=[channel_cls.key for channel_cls in enabled_channel_classes],
7772
subject=subject,
7873
text=text,
7974
url=url,
@@ -87,6 +82,7 @@ def send_notification(
8782
notification.save()
8883

8984
# Process through enabled channels only
85+
enabled_channel_instances = [channel_cls() for channel_cls in enabled_channel_classes]
9086
for channel_instance in enabled_channel_instances:
9187
try:
9288
channel_instance.process(notification)

generic_notifications/channels.py

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.template.loader import render_to_string
99
from django.utils import timezone
1010

11-
from .frequencies import DailyFrequency, NotificationFrequency
11+
from .frequencies import NotificationFrequency
1212
from .registry import registry
1313

1414
if TYPE_CHECKING:
@@ -33,23 +33,6 @@ def process(self, notification: "Notification") -> None:
3333
"""
3434
pass
3535

36-
def is_enabled(self, user: Any, notification_type: str) -> bool:
37-
"""
38-
Check if user has this channel enabled for this notification type.
39-
40-
Args:
41-
user: User instance
42-
notification_type: Notification type key
43-
44-
Returns:
45-
bool: True if enabled (default), False if disabled
46-
"""
47-
from .models import DisabledNotificationTypeChannel
48-
49-
return not DisabledNotificationTypeChannel.objects.filter(
50-
user=user, notification_type=notification_type, channel=self.key
51-
).exists()
52-
5336

5437
def register(cls: Type[NotificationChannel]) -> Type[NotificationChannel]:
5538
"""
@@ -106,37 +89,14 @@ def process(self, notification: "Notification") -> None:
10689
Args:
10790
notification: Notification instance to process
10891
"""
109-
frequency = self.get_frequency(notification.recipient, notification.notification_type)
92+
# Get notification type class from key
93+
notification_type_cls = registry.get_type(notification.notification_type)
94+
frequency_cls = notification_type_cls.get_email_frequency(notification.recipient)
11095

11196
# Send immediately if realtime, otherwise leave for digest
112-
if frequency and frequency.is_realtime:
97+
if frequency_cls and frequency_cls.is_realtime:
11398
self.send_email_now(notification)
11499

115-
def get_frequency(self, user: Any, notification_type: str) -> NotificationFrequency:
116-
"""
117-
Get the user's email frequency preference for this notification type.
118-
119-
Args:
120-
user: User instance
121-
notification_type: Notification type key
122-
123-
Returns:
124-
NotificationFrequency: NotificationFrequency instance (defaults to notification type's default)
125-
"""
126-
from .models import EmailFrequency
127-
128-
try:
129-
email_frequency = EmailFrequency.objects.get(user=user, notification_type=notification_type)
130-
return registry.get_frequency(email_frequency.frequency)
131-
except (EmailFrequency.DoesNotExist, KeyError):
132-
# Get the notification type's default frequency
133-
try:
134-
notification_type_obj = registry.get_type(notification_type)
135-
return notification_type_obj.default_email_frequency()
136-
except (KeyError, AttributeError):
137-
# Fallback to realtime if notification type not found or no default
138-
return DailyFrequency()
139-
140100
def send_email_now(self, notification: "Notification") -> None:
141101
"""
142102
Send an individual email notification immediately.
@@ -196,7 +156,7 @@ def send_email_now(self, notification: "Notification") -> None:
196156

197157
@classmethod
198158
def send_digest_emails(
199-
cls, user: Any, notifications: "QuerySet[Notification]", frequency: NotificationFrequency | None = None
159+
cls, user: Any, notifications: "QuerySet[Notification]", frequency: type[NotificationFrequency] | None = None
200160
):
201161
"""
202162
Send a digest email to a specific user with specific notifications.
@@ -207,14 +167,12 @@ def send_digest_emails(
207167
notifications: QuerySet of notifications to include in digest
208168
frequency: The frequency for template context
209169
"""
210-
from .models import Notification
211-
212170
if not notifications.exists():
213171
return
214172

215173
try:
216174
# Group notifications by type for better digest formatting
217-
notifications_by_type: dict[str, list[Notification]] = {}
175+
notifications_by_type: dict[str, list["Notification"]] = {}
218176
for notification in notifications:
219177
if notification.notification_type not in notifications_by_type:
220178
notifications_by_type[notification.notification_type] = []

generic_notifications/management/commands/send_digest_emails.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import logging
22

33
from django.contrib.auth import get_user_model
4+
from django.contrib.auth.models import AbstractUser
45
from django.core.management.base import BaseCommand
56

67
from generic_notifications.channels import EmailChannel
8+
from generic_notifications.frequencies import NotificationFrequency
79
from generic_notifications.models import Notification
810
from generic_notifications.registry import registry
11+
from generic_notifications.types import NotificationType
912

1013
User = get_user_model()
1114

@@ -48,23 +51,22 @@ def handle(self, *args, **options):
4851
return
4952

5053
# Setup
51-
email_channel = EmailChannel()
5254
all_notification_types = registry.get_all_types()
5355

5456
# Get the specific frequency (required argument)
5557
try:
56-
frequency = registry.get_frequency(target_frequency)
58+
frequency_cls = registry.get_frequency(target_frequency)
5759
except KeyError:
5860
logger.error(f"Frequency '{target_frequency}' not found")
5961
return
6062

61-
if frequency.is_realtime:
63+
if frequency_cls.is_realtime:
6264
logger.error(f"Frequency '{target_frequency}' is realtime, not a digest frequency")
6365
return
6466

6567
total_emails_sent = 0
6668

67-
logger.info(f"Processing {frequency.name} digests...")
69+
logger.info(f"Processing {frequency_cls.name} digests...")
6870

6971
# Find all users who have unsent, unread notifications for email channel
7072
users_with_notifications = User.objects.filter(
@@ -75,31 +77,29 @@ def handle(self, *args, **options):
7577

7678
for user in users_with_notifications:
7779
# Determine which notification types should use this frequency for this user
78-
relevant_types = self.get_notification_types_for_frequency(
79-
user,
80-
frequency.key,
81-
all_notification_types,
82-
email_channel,
83-
)
80+
relevant_types = self.get_notification_types_for_frequency(user, frequency_cls, all_notification_types)
8481

8582
if not relevant_types:
8683
continue
8784

8885
# Get unsent notifications for these types
8986
# Exclude read notifications - don't email what user already saw on website
87+
relevant_type_keys = [nt.key for nt in relevant_types]
9088
notifications = Notification.objects.filter(
9189
recipient=user,
92-
notification_type__in=relevant_types,
90+
notification_type__in=relevant_type_keys,
9391
email_sent_at__isnull=True,
9492
read__isnull=True,
9593
channels__icontains=f'"{EmailChannel.key}"',
9694
).order_by("-added")
9795

9896
if notifications.exists():
99-
logger.info(f" User {user.email}: {notifications.count()} notifications for {frequency.name} digest")
97+
logger.info(
98+
f" User {user.email}: {notifications.count()} notifications for {frequency_cls.name} digest"
99+
)
100100

101101
if not dry_run:
102-
EmailChannel.send_digest_emails(user, notifications, frequency)
102+
EmailChannel.send_digest_emails(user, notifications, frequency_cls)
103103

104104
total_emails_sent += 1
105105

@@ -117,18 +117,30 @@ def handle(self, *args, **options):
117117
else:
118118
logger.info(f"Successfully sent {total_emails_sent} digest emails")
119119

120-
def get_notification_types_for_frequency(self, user, frequency_key, all_notification_types, email_channel):
120+
def get_notification_types_for_frequency(
121+
self,
122+
user: AbstractUser,
123+
wanted_frequency: type[NotificationFrequency],
124+
all_notification_types: list[type["NotificationType"]],
125+
) -> list[type["NotificationType"]]:
121126
"""
122127
Get all notification types that should use this frequency for the given user.
123128
This includes both explicit preferences and types that default to this frequency.
124129
Since notifications are only created for enabled channels, we don't need to check is_enabled.
130+
131+
Args:
132+
user: The user to check preferences for
133+
wanted_frequency: The frequency to filter by (e.g. DailyFrequency, RealtimeFrequency)
134+
all_notification_types: List of all registered notification type classes
135+
136+
Returns:
137+
List of notification type classes that use this frequency for this user
125138
"""
126-
relevant_types = set()
139+
relevant_types: list[type["NotificationType"]] = []
127140

128141
for notification_type in all_notification_types:
129-
# Use EmailChannel's get_frequency method to get the frequency for this user/type
130-
user_frequency = email_channel.get_frequency(user, notification_type.key)
131-
if user_frequency.key == frequency_key:
132-
relevant_types.add(notification_type.key)
142+
user_frequency = notification_type.get_email_frequency(user)
143+
if user_frequency.key == wanted_frequency.key:
144+
relevant_types.append(notification_type)
133145

134-
return list(relevant_types)
146+
return relevant_types

generic_notifications/models.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class Meta:
4343

4444
def clean(self):
4545
try:
46-
notification_type_obj = registry.get_type(self.notification_type)
46+
notification_type_cls = registry.get_type(self.notification_type)
4747
except KeyError:
4848
available_types = [t.key for t in registry.get_all_types()]
4949
if available_types:
@@ -56,10 +56,10 @@ def clean(self):
5656
)
5757

5858
# Check if trying to disable a required channel
59-
required_channel_keys = [cls.key for cls in notification_type_obj.required_channels]
59+
required_channel_keys = [cls.key for cls in notification_type_cls.required_channels]
6060
if self.channel in required_channel_keys:
6161
raise ValidationError(
62-
f"Cannot disable {self.channel} channel for {notification_type_obj.name} - this channel is required"
62+
f"Cannot disable {self.channel} channel for {notification_type_cls.name} - this channel is required"
6363
)
6464

6565
try:
@@ -204,7 +204,8 @@ def get_subject(self) -> str:
204204

205205
# Get the notification type and use its dynamic generation
206206
try:
207-
notification_type = registry.get_type(self.notification_type)
207+
notification_type_cls = registry.get_type(self.notification_type)
208+
notification_type = notification_type_cls()
208209
return notification_type.get_subject(self) or notification_type.description
209210
except KeyError:
210211
return f"Notification: {self.notification_type}"
@@ -216,7 +217,8 @@ def get_text(self) -> str:
216217

217218
# Get the notification type and use its dynamic generation
218219
try:
219-
notification_type = registry.get_type(self.notification_type)
220+
notification_type_cls = registry.get_type(self.notification_type)
221+
notification_type = notification_type_cls()
220222
return notification_type.get_text(self)
221223
except KeyError:
222224
return "You have a new notification"

generic_notifications/preferences.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from typing import TYPE_CHECKING, Any, Dict, List
1+
from typing import Any, Dict, List
2+
3+
from django.contrib.auth.models import AbstractUser
24

35
from .models import DisabledNotificationTypeChannel, EmailFrequency
46
from .registry import registry
57

6-
if TYPE_CHECKING:
7-
from django.contrib.auth.models import AbstractUser
8-
98

109
def get_notification_preferences(user: "AbstractUser") -> List[Dict[str, Any]]:
1110
"""
@@ -90,16 +89,15 @@ def save_notification_preferences(user: "AbstractUser", form_data: Dict[str, Any
9089

9190
# If checkbox not checked, create disabled entry
9291
if form_key not in form_data:
93-
DisabledNotificationTypeChannel.objects.create(
94-
user=user, notification_type=type_key, channel=channel_key
95-
)
92+
notification_type.disable_channel(user=user, channel=channel)
9693

9794
# Handle email frequency preference
9895
if "email" in [ch.key for ch in channels.values()]:
9996
frequency_key = f"{type_key}__frequency"
10097
if frequency_key in form_data:
10198
frequency_value = form_data[frequency_key]
10299
if frequency_value in frequencies:
100+
frequency_obj = frequencies[frequency_value]
103101
# Only save if different from default
104102
if frequency_value != notification_type.default_email_frequency.key:
105-
EmailFrequency.objects.create(user=user, notification_type=type_key, frequency=frequency_value)
103+
notification_type.set_email_frequency(user=user, frequency=frequency_obj)

0 commit comments

Comments
 (0)