Skip to content

Commit 76706f7

Browse files
committed
feat: Channels should have some way to determine if they can send the notification (closes #19)
1 parent 580e6d2 commit 76706f7

File tree

4 files changed

+142
-148
lines changed

4 files changed

+142
-148
lines changed

generic_notifications/__init__.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,22 @@ def send_notification(
8080
if notification_type.should_save(notification):
8181
notification.save()
8282

83-
# Create NotificationChannel entries for each enabled channel
83+
# Check should_send and process for each enabled channel
8484
for channel_cls in enabled_channel_classes:
85-
NotificationChannel.objects.create(
86-
notification=notification,
87-
channel=channel_cls.key,
88-
)
89-
90-
# Process through enabled channels only
91-
enabled_channel_instances = [channel_cls() for channel_cls in enabled_channel_classes]
92-
for channel_instance in enabled_channel_instances:
93-
try:
94-
channel_instance.process(notification)
95-
except Exception as e:
96-
# Log error but don't crash - other channels should still work
97-
logger = logging.getLogger(__name__)
98-
logger.error(
99-
f"Channel {channel_instance.key} failed to process notification {notification.id}: {e}"
85+
if channel_cls.should_send(notification):
86+
# Create NotificationChannel entry only for channels that can send
87+
NotificationChannel.objects.create(
88+
notification=notification,
89+
channel=channel_cls.key,
10090
)
10191

92+
# Process the notification through this channel
93+
try:
94+
channel_instance = channel_cls()
95+
channel_instance.process(notification)
96+
except Exception as e:
97+
# Log error but don't crash - other channels should still work
98+
logger = logging.getLogger(__name__)
99+
logger.error(f"Channel {channel_cls.key} failed to process notification {notification.id}: {e}")
100+
102101
return notification

generic_notifications/channels.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ class BaseChannel(ABC):
2525
supports_realtime: bool = True
2626
supports_digest: bool = False
2727

28+
@classmethod
29+
def should_send(cls, notification: "Notification") -> bool:
30+
"""
31+
Check if this channel can send the given notification.
32+
Override in subclasses to add channel-specific validation.
33+
34+
Args:
35+
notification: Notification instance to check
36+
37+
Returns:
38+
bool: True if the channel can send this notification, False otherwise
39+
"""
40+
return True
41+
2842
def process(self, notification: "Notification") -> None:
2943
"""
3044
Process a notification through this channel based on channel capabilities
@@ -64,9 +78,7 @@ def send_now(self, notification: "Notification") -> None:
6478
"""
6579
raise NotImplementedError(f"{self.__class__.__name__} does not support realtime sending")
6680

67-
def send_digest(
68-
self, notifications: "QuerySet[Notification]", frequency: type[BaseFrequency] | None = None
69-
) -> None:
81+
def send_digest(self, notifications: "QuerySet[Notification]", frequency: type[BaseFrequency]) -> None:
7082
"""
7183
Send a digest with specific notifications.
7284
Override in subclasses that support digest delivery.
@@ -129,6 +141,19 @@ class EmailChannel(BaseChannel):
129141
supports_realtime = True
130142
supports_digest = True
131143

144+
@classmethod
145+
def should_send(cls, notification: "Notification") -> bool:
146+
"""
147+
Check if the recipient has an email address.
148+
149+
Args:
150+
notification: Notification instance to check
151+
152+
Returns:
153+
bool: True if the recipient has an email address, False otherwise
154+
"""
155+
return bool(getattr(notification.recipient, "email", None))
156+
132157
def send_now(self, notification: "Notification") -> None:
133158
"""
134159
Send an individual email notification immediately.
@@ -198,7 +223,7 @@ def send_now(self, notification: "Notification") -> None:
198223
logger = logging.getLogger(__name__)
199224
logger.error(f"Failed to send email for notification {notification.id}: {e}")
200225

201-
def send_digest(self, notifications: "QuerySet[Notification]", frequency: type[BaseFrequency] | None = None):
226+
def send_digest(self, notifications: "QuerySet[Notification]", frequency: type[BaseFrequency]):
202227
"""
203228
Send a digest email with specific notifications.
204229
This method is used by the management command.
@@ -240,8 +265,7 @@ def send_digest(self, notifications: "QuerySet[Notification]", frequency: type[B
240265
subject = render_to_string(subject_template, context).strip()
241266
except Exception:
242267
# Fallback subject
243-
frequency_name = frequency.name if frequency else "Digest"
244-
subject = f"{frequency_name} - {notifications_count} new notification{pluralize(notifications_count)}"
268+
subject = f"{frequency.name} - {notifications_count} new notification{pluralize(notifications_count)}"
245269

246270
# Load HTML message
247271
try:

tests/test_channels.py

Lines changed: 32 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.test import TestCase, override_settings
88

99
from generic_notifications.channels import BaseChannel, EmailChannel
10-
from generic_notifications.frequencies import RealtimeFrequency
10+
from generic_notifications.frequencies import DailyFrequency, RealtimeFrequency
1111
from generic_notifications.models import DisabledNotificationTypeChannel, Notification, NotificationFrequency
1212
from generic_notifications.registry import registry
1313
from generic_notifications.types import NotificationType
@@ -167,6 +167,29 @@ def test_process_digest_frequency(self):
167167
notification.refresh_from_db()
168168
self.assertFalse(notification.is_sent_on_channel(EmailChannel))
169169

170+
def test_should_send_with_email(self):
171+
"""Test that should_send returns True when user has email"""
172+
notification = create_notification_with_channels(
173+
user=self.user,
174+
notification_type="test_type",
175+
)
176+
177+
self.assertTrue(EmailChannel.should_send(notification))
178+
179+
def test_should_send_without_email(self):
180+
"""Test that should_send returns False when user has no email"""
181+
# Create user without email
182+
user_no_email = User.objects.create_user(username="user2", password="testpass")
183+
user_no_email.email = ""
184+
user_no_email.save()
185+
186+
notification = create_notification_with_channels(
187+
user=user_no_email,
188+
notification_type="test_type",
189+
)
190+
191+
self.assertFalse(EmailChannel.should_send(notification))
192+
170193
@override_settings(DEFAULT_FROM_EMAIL="test@example.com")
171194
def test_send_now_basic(self):
172195
notification = create_notification_with_channels(
@@ -312,15 +335,6 @@ def test_send_now_template_error_fallback(self):
312335
self.assertEqual(email.subject, "Test Subject")
313336
self.assertEqual(len(email.alternatives), 0) # type: ignore[attr-defined] # No HTML alternative
314337

315-
@override_settings(DEFAULT_FROM_EMAIL="test@example.com")
316-
def test_send_digest_emails_empty_queryset(self):
317-
# No notifications exist, so digest should not send anything
318-
empty_notifications = Notification.objects.none()
319-
EmailChannel().send_digest(empty_notifications)
320-
321-
# No email should be sent when no notifications exist
322-
self.assertEqual(len(mail.outbox), 0)
323-
324338
@override_settings(DEFAULT_FROM_EMAIL="test@example.com")
325339
def test_send_digest_emails_basic(self):
326340
# Set user to daily frequency to prevent realtime sending
@@ -342,13 +356,13 @@ def test_send_digest_emails_basic(self):
342356
)
343357

344358
# Send digest email for this user
345-
EmailChannel().send_digest(notifications)
359+
EmailChannel().send_digest(notifications, DailyFrequency)
346360

347361
# Check email was sent
348362
self.assertEqual(len(mail.outbox), 1)
349363
email = mail.outbox[0]
350364
self.assertEqual(email.to, [self.user.email])
351-
self.assertEqual(email.subject, "Digest - 3 new notifications")
365+
self.assertEqual(email.subject, "Daily digest - 3 new notifications")
352366

353367
# Check all notifications marked as sent
354368
for notification in notifications:
@@ -371,98 +385,12 @@ def test_send_digest_emails_with_frequency(self):
371385
recipient=self.user,
372386
channels__channel="email",
373387
channels__sent_at__isnull=True,
374-
)
375-
)
376-
377-
email = mail.outbox[0]
378-
self.assertEqual(email.subject, "Digest - 1 new notification")
379-
380-
@override_settings(DEFAULT_FROM_EMAIL="test@example.com")
381-
def test_send_digest_emails_without_frequency(self):
382-
# Set user to daily frequency to prevent realtime sending
383-
NotificationFrequency.objects.create(user=self.user, notification_type="test_type", frequency="daily")
384-
385-
create_notification_with_channels(
386-
user=self.user,
387-
notification_type="test_type",
388-
subject="Test",
389-
)
390-
391-
EmailChannel().send_digest(
392-
Notification.objects.filter(
393-
recipient=self.user,
394-
channels__channel="email",
395-
channels__sent_at__isnull=True,
396-
)
397-
)
398-
399-
email = mail.outbox[0]
400-
self.assertEqual(email.subject, "Digest - 1 new notification")
401-
402-
@override_settings(DEFAULT_FROM_EMAIL="test@example.com")
403-
def test_send_digest_emails_text_limit(self):
404-
# Set user to daily frequency to prevent realtime sending
405-
NotificationFrequency.objects.create(user=self.user, notification_type="test_type", frequency="daily")
406-
407-
# Create more than 10 notifications to test text limit
408-
for i in range(15):
409-
create_notification_with_channels(
410-
user=self.user,
411-
notification_type="test_type",
412-
subject=f"Test {i}",
413-
)
414-
415-
EmailChannel().send_digest(
416-
Notification.objects.filter(
417-
recipient=self.user,
418-
channels__channel="email",
419-
channels__sent_at__isnull=True,
420-
)
388+
),
389+
DailyFrequency,
421390
)
422391

423-
# The implementation may not have this feature, so we'll just check that email was sent
424-
self.assertEqual(len(mail.outbox), 1)
425392
email = mail.outbox[0]
426-
self.assertEqual(email.subject, "Digest - 15 new notifications")
427-
428-
@override_settings(DEFAULT_FROM_EMAIL="test@example.com")
429-
@patch("generic_notifications.channels.render_to_string")
430-
def test_send_digest_emails_with_html_template(self, mock_render):
431-
mock_render.return_value = "<html>Digest HTML</html>"
432-
433-
# Set user to daily frequency to prevent realtime sending
434-
NotificationFrequency.objects.create(user=self.user, notification_type="test_type", frequency="daily")
435-
436-
create_notification_with_channels(
437-
user=self.user,
438-
notification_type="test_type",
439-
subject="Test",
440-
)
441-
442-
EmailChannel().send_digest(
443-
Notification.objects.filter(
444-
recipient=self.user,
445-
channels__channel="email",
446-
channels__sent_at__isnull=True,
447-
)
448-
)
449-
450-
# Check templates were rendered (subject, HTML, then text)
451-
self.assertEqual(mock_render.call_count, 3)
452-
453-
# Check subject template call
454-
subject_call = mock_render.call_args_list[0]
455-
self.assertEqual(subject_call[0][0], "notifications/email/digest/subject.txt")
456-
457-
# Check HTML template call
458-
html_call = mock_render.call_args_list[1]
459-
self.assertEqual(html_call[0][0], "notifications/email/digest/message.html")
460-
461-
# Check text template call
462-
text_call = mock_render.call_args_list[2]
463-
self.assertEqual(text_call[0][0], "notifications/email/digest/message.txt")
464-
self.assertEqual(html_call[0][1]["user"], self.user)
465-
self.assertEqual(html_call[0][1]["count"], 1)
393+
self.assertEqual(email.subject, "Daily digest - 1 new notification")
466394

467395
def test_send_now_fallback_includes_url(self):
468396
"""Test that fallback email content includes URL when available"""
@@ -505,7 +433,8 @@ def test_send_digest_emails_fallback_includes_urls(self):
505433
recipient=self.user,
506434
channels__channel="email",
507435
channels__sent_at__isnull=True,
508-
)
436+
),
437+
DailyFrequency,
509438
)
510439

511440
# Check that one email was sent
@@ -584,7 +513,7 @@ def send_email(
584513

585514
# Test the custom channel
586515
custom_channel = TestEmailChannel()
587-
custom_channel.send_digest(notifications)
516+
custom_channel.send_digest(notifications, DailyFrequency)
588517

589518
# Verify the custom _send_email method was called
590519
self.assertEqual(len(custom_channel.sent_emails), 1)

0 commit comments

Comments
 (0)