django-generic-notifications


Namedjango-generic-notifications JSON
Version 1.0.0 PyPI version JSON
download
home_pageNone
SummaryA flexible, multi-channel notification system for Django applications with built-in support for email digests, user preferences, and extensible delivery channels.
upload_time2025-08-01 19:53:48
maintainerNone
docs_urlNone
authorKevin Renskers
requires_python>=3.10
licenseNone
keywords django notifications email digest multi-channel
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Django Generic Notifications

A flexible, multi-channel notification system for Django applications with built-in support for email digests, user preferences, and extensible delivery channels.

## Features

- **Multi-channel delivery**: Send notifications through multiple channels (website, email, and custom channels)
- **Flexible email frequencies**: Support for real-time and digest emails (daily, or custom schedules)
- **Notification grouping**: Prevent repeated notifications by grouping notifications based on your own custom logic
- **User preferences**: Fine-grained control over notification types and delivery channels
- **Extensible architecture**: Easy to add custom notification types, channels, and frequencies
- **Generic relations**: Link notifications to any Django model
- **Template support**: Customizable email templates for each notification type
- **Developer friendly**: Simple API for sending notifications with automatic channel routing
- **Full type hints**: Complete type annotations for better IDE support and type checking

## Installation

All instruction in this document use [uv](https://github.com/astral-sh/uv), but of course pip or Poetry will also work just fine.

```bash
uv add django-generic-notifications
```

Add to your `INSTALLED_APPS`:

```python
INSTALLED_APPS = [
    ...
    "generic_notifications",
    ...
]
```

Run migrations:

```bash
uv run ./manage.py migrate generic_notifications
```

## Quick Start

### 1. Define a notification type

```python
# myapp/notifications.py
from generic_notifications.types import NotificationType, register

@register
class CommentNotification(NotificationType):
    key = "comment"
    name = "Comment Notifications"
    description = "When someone comments on your posts"
```

### 2. Send a notification

```python
from generic_notifications import send_notification
from myapp.notifications import CommentNotification

# Send a notification (only `recipient` and `notification_type` are required)
notification = send_notification(
    recipient=post.author,
    notification_type=CommentNotification,
    actor=comment.user,
    target=post,
    subject=f"{comment.user.get_full_name()} commented on your post",
    text=f"{comment.user.get_full_name()} left a comment: {comment.text[:100]}",
    url=f"/posts/{post.id}#comment-{comment.id}",
)
```

### 3. Set up email digest sending

Create a cron job to send daily digests:

```bash
# Send daily digests at 9 AM
0 9 * * * cd /path/to/project && uv run ./manage.py send_digest_emails --frequency daily
```

## User Preferences

By default every user gets notifications of all registered types delivered to every registered channel, but users can opt-out of receiving notification types, per channel.

All notification types default to daily digest, except for `SystemMessage` which defaults to real-time. Users can choose  different frequency per notification type.

```python
from generic_notifications.models import DisabledNotificationTypeChannel, EmailFrequency
from generic_notifications.channels import EmailChannel
from generic_notifications.frequencies import RealtimeFrequency
from myapp.notifications import CommentNotification

# Disable email channel for comment notifications
DisabledNotificationTypeChannel.objects.create(
    user=user,
    notification_type=CommentNotification.key,
    channel=EmailChannel.key
)

# Change to realtime digest for a notification type
EmailFrequency.objects.update_or_create(
    user=user,
    notification_type="comment",
    defaults={'frequency': RealtimeFrequency.key}
)
```

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).

## Custom Channels

Create custom delivery channels:

```python
from generic_notifications.channels import NotificationChannel, register

@register
class SMSChannel(NotificationChannel):
    key = "sms"
    name = "SMS"

    def process(self, notification):
        # Send SMS using your preferred service
        send_sms(
            to=notification.recipient.phone_number,
            message=notification.get_text()
        )
```

## Custom Frequencies

Add custom email frequencies:

```python
from generic_notifications.frequencies import NotificationFrequency, register

@register
class WeeklyFrequency(NotificationFrequency):
    key = "weekly"
    name = "Weekly digest"
    is_realtime = False
    description = "Receive a weekly summary every Monday"
```

When you add custom email frequencies you’ll have to run `send_digest_emails` for them as well. For example, if you created that weekly digest:

```bash
# Send weekly digest every Monday at 9 AM
0 9 * * 1 cd /path/to/project && uv run ./manage.py send_digest_emails --frequency weekly
```

## Email Templates

Customize email templates by creating these files in your templates directory:

### Real-time emails

- `notifications/email/realtime/{notification_type}_subject.txt`
- `notifications/email/realtime/{notification_type}.html`
- `notifications/email/realtime/{notification_type}.txt`

### Digest emails

- `notifications/email/digest/subject.txt`
- `notifications/email/digest/message.html`
- `notifications/email/digest/message.txt`

## Advanced Usage

### Required Channels

Make certain channels mandatory for critical notifications:

```python
from generic_notifications.channels import EmailChannel

@register
class SecurityAlert(NotificationType):
    key = "security_alert"
    name = "Security Alerts"
    description = "Important security notifications"
    required_channels = [EmailChannel]  # Cannot be disabled
```

### Querying Notifications

```python
from generic_notifications.models import Notification
from generic_notifications.lib import get_unread_count, get_notifications, mark_notifications_as_read

# Get unread count for a user
unread_count = get_unread_count(user=user, channel=WebsiteChannel)

# Get unread notifications for a user
unread_notifications = get_notifications(user=user, channel=WebsiteChannel, unread_only=True)

# Get notifications by channel
email_notifications = Notification.objects.for_channel(WebsiteChannel)

# Mark as read
notification.mark_as_read()

# Mark all as read
mark_notifications_as_read(user=user)
```

### Notification Grouping

Prevent notification spam by grouping similar notifications together. Instead of creating multiple "You received a comment" notifications, you can update an existing notification to say "You received 3 comments".

```python
@register
class CommentNotification(NotificationType):
    key = "comment"
    name = "Comment Notifications"
    description = "When someone comments on your posts"

    @classmethod
    def should_save(cls, notification):
        # Look for existing unread notification with same actor and target
        existing = Notification.objects.filter(
            recipient=notification.recipient,
            notification_type=notification.notification_type,
            actor=notification.actor,
            content_type_id=notification.content_type_id,
            object_id=notification.object_id,
            read__isnull=True,
        ).first()

        if existing:
            # Update count in metadata
            count = existing.metadata.get("count", 1)
            existing.metadata["count"] = count + 1
            existing.save()
            return False  # Don't create new notification

        # First notification of this type, so it should be saved
        return True

    def get_text(self, notification):
        count = notification.metadata.get("count", 1)
        actor_name = notification.actor.get_full_name()

        if count == 1:
            return f"{actor_name} commented on your post"
        else:
            return f"{actor_name} left {count} comments on your post"
```

The `should_save` method is called before saving each notification. Return `False` to prevent creating a new notification and instead update an existing one. This gives you complete control over grouping logic - you might group by time windows, actors, targets, or any other criteria.

## Performance Considerations

### Accessing `notification.target`

While you can store any object into a notification's `target` field, it's usually not a great idea to use this field to dynamically create the notification's subject and text, as the `target` generic relationship can't be prefetched more than one level deep.

In other words, something like this will cause an N+1 query problem when you show a list of notifications in a table, for example:

```python
class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    comment_text = models.TextField()


@register
class CommentNotificationType(NotificationType):
    key = "comment_notification"
    name = "Comments"
    description = "You received a comment"

    def get_text(self, notification):
          actor_name = notification.actor.full_name
          article = notification.target.article
          comment_text = notification.target.comment.comment_text
          return f'{actor_name} commented on your article "{article.title}": "{comment_text}"'

```

The problem is `target.article`, which cannot be prefetched and thus causes another query for every notification. This is why it’s better to store the subject, text and url in the notification itself, rather than relying on `target` dynamically.

### Non-blocking email sending

The email channel (EmailChannel) will send real-time emails using Django’s built-in `send_mail` method. This is a blocking function call, meaning that while a connection with the SMTP server is made and the email is sent off, the process that’s sending the notification has to wait. This is not ideal, but easily solved by using something like [django-mailer](https://github.com/pinax/django-mailer/), which provides a queueing backend for `send_mail`. This means that sending email no longer is a blocking action

## Example app

An example app is provided, which shows how to create a custom notification type, how to send a notification, it has a nice looking notification center with unread notifications as well as an archive of all read notifications, plus a settings view where you can manage notification preferences.

```bash
cd example
uv run ./manage.py migrate
uv run ./manage.py runserver
```

Then open http://127.0.0.1:8000/.

## Development

### Setup

```bash
# Clone the repository
git clone https://github.com/loopwerk/django-generic-notifications.git
cd django-generic-notifications
```

### Testing

```bash
# Run all tests
uv run pytest
```

### Code Quality

```bash
# Type checking
uv run mypy .

# Linting
uv run ruff check .

# Formatting
uv run ruff format .
```

## License

MIT License - see LICENSE file for details.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "django-generic-notifications",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "django, notifications, email, digest, multi-channel",
    "author": "Kevin Renskers",
    "author_email": "Kevin Renskers <kevin@loopwerk.io>",
    "download_url": "https://files.pythonhosted.org/packages/14/93/19613eb5cc576dbc99bf2f0e948ccbe1ea7d7efb4181527c07291ce11751/django_generic_notifications-1.0.0.tar.gz",
    "platform": null,
    "description": "# Django Generic Notifications\n\nA flexible, multi-channel notification system for Django applications with built-in support for email digests, user preferences, and extensible delivery channels.\n\n## Features\n\n- **Multi-channel delivery**: Send notifications through multiple channels (website, email, and custom channels)\n- **Flexible email frequencies**: Support for real-time and digest emails (daily, or custom schedules)\n- **Notification grouping**: Prevent repeated notifications by grouping notifications based on your own custom logic\n- **User preferences**: Fine-grained control over notification types and delivery channels\n- **Extensible architecture**: Easy to add custom notification types, channels, and frequencies\n- **Generic relations**: Link notifications to any Django model\n- **Template support**: Customizable email templates for each notification type\n- **Developer friendly**: Simple API for sending notifications with automatic channel routing\n- **Full type hints**: Complete type annotations for better IDE support and type checking\n\n## Installation\n\nAll instruction in this document use [uv](https://github.com/astral-sh/uv), but of course pip or Poetry will also work just fine.\n\n```bash\nuv add django-generic-notifications\n```\n\nAdd to your `INSTALLED_APPS`:\n\n```python\nINSTALLED_APPS = [\n    ...\n    \"generic_notifications\",\n    ...\n]\n```\n\nRun migrations:\n\n```bash\nuv run ./manage.py migrate generic_notifications\n```\n\n## Quick Start\n\n### 1. Define a notification type\n\n```python\n# myapp/notifications.py\nfrom generic_notifications.types import NotificationType, register\n\n@register\nclass CommentNotification(NotificationType):\n    key = \"comment\"\n    name = \"Comment Notifications\"\n    description = \"When someone comments on your posts\"\n```\n\n### 2. Send a notification\n\n```python\nfrom generic_notifications import send_notification\nfrom myapp.notifications import CommentNotification\n\n# Send a notification (only `recipient` and `notification_type` are required)\nnotification = send_notification(\n    recipient=post.author,\n    notification_type=CommentNotification,\n    actor=comment.user,\n    target=post,\n    subject=f\"{comment.user.get_full_name()} commented on your post\",\n    text=f\"{comment.user.get_full_name()} left a comment: {comment.text[:100]}\",\n    url=f\"/posts/{post.id}#comment-{comment.id}\",\n)\n```\n\n### 3. Set up email digest sending\n\nCreate a cron job to send daily digests:\n\n```bash\n# Send daily digests at 9 AM\n0 9 * * * cd /path/to/project && uv run ./manage.py send_digest_emails --frequency daily\n```\n\n## User Preferences\n\nBy default every user gets notifications of all registered types delivered to every registered channel, but users can opt-out of receiving notification types, per channel.\n\nAll notification types default to daily digest, except for `SystemMessage` which defaults to real-time. Users can choose  different frequency per notification type.\n\n```python\nfrom generic_notifications.models import DisabledNotificationTypeChannel, EmailFrequency\nfrom generic_notifications.channels import EmailChannel\nfrom generic_notifications.frequencies import RealtimeFrequency\nfrom myapp.notifications import CommentNotification\n\n# Disable email channel for comment notifications\nDisabledNotificationTypeChannel.objects.create(\n    user=user,\n    notification_type=CommentNotification.key,\n    channel=EmailChannel.key\n)\n\n# Change to realtime digest for a notification type\nEmailFrequency.objects.update_or_create(\n    user=user,\n    notification_type=\"comment\",\n    defaults={'frequency': RealtimeFrequency.key}\n)\n```\n\nThis project doesn\u2019t come with a UI (view + template) for managing user preferences, but an example is provided in the [example app](#example-app).\n\n## Custom Channels\n\nCreate custom delivery channels:\n\n```python\nfrom generic_notifications.channels import NotificationChannel, register\n\n@register\nclass SMSChannel(NotificationChannel):\n    key = \"sms\"\n    name = \"SMS\"\n\n    def process(self, notification):\n        # Send SMS using your preferred service\n        send_sms(\n            to=notification.recipient.phone_number,\n            message=notification.get_text()\n        )\n```\n\n## Custom Frequencies\n\nAdd custom email frequencies:\n\n```python\nfrom generic_notifications.frequencies import NotificationFrequency, register\n\n@register\nclass WeeklyFrequency(NotificationFrequency):\n    key = \"weekly\"\n    name = \"Weekly digest\"\n    is_realtime = False\n    description = \"Receive a weekly summary every Monday\"\n```\n\nWhen you add custom email frequencies you\u2019ll have to run `send_digest_emails` for them as well. For example, if you created that weekly digest:\n\n```bash\n# Send weekly digest every Monday at 9 AM\n0 9 * * 1 cd /path/to/project && uv run ./manage.py send_digest_emails --frequency weekly\n```\n\n## Email Templates\n\nCustomize email templates by creating these files in your templates directory:\n\n### Real-time emails\n\n- `notifications/email/realtime/{notification_type}_subject.txt`\n- `notifications/email/realtime/{notification_type}.html`\n- `notifications/email/realtime/{notification_type}.txt`\n\n### Digest emails\n\n- `notifications/email/digest/subject.txt`\n- `notifications/email/digest/message.html`\n- `notifications/email/digest/message.txt`\n\n## Advanced Usage\n\n### Required Channels\n\nMake certain channels mandatory for critical notifications:\n\n```python\nfrom generic_notifications.channels import EmailChannel\n\n@register\nclass SecurityAlert(NotificationType):\n    key = \"security_alert\"\n    name = \"Security Alerts\"\n    description = \"Important security notifications\"\n    required_channels = [EmailChannel]  # Cannot be disabled\n```\n\n### Querying Notifications\n\n```python\nfrom generic_notifications.models import Notification\nfrom generic_notifications.lib import get_unread_count, get_notifications, mark_notifications_as_read\n\n# Get unread count for a user\nunread_count = get_unread_count(user=user, channel=WebsiteChannel)\n\n# Get unread notifications for a user\nunread_notifications = get_notifications(user=user, channel=WebsiteChannel, unread_only=True)\n\n# Get notifications by channel\nemail_notifications = Notification.objects.for_channel(WebsiteChannel)\n\n# Mark as read\nnotification.mark_as_read()\n\n# Mark all as read\nmark_notifications_as_read(user=user)\n```\n\n### Notification Grouping\n\nPrevent notification spam by grouping similar notifications together. Instead of creating multiple \"You received a comment\" notifications, you can update an existing notification to say \"You received 3 comments\".\n\n```python\n@register\nclass CommentNotification(NotificationType):\n    key = \"comment\"\n    name = \"Comment Notifications\"\n    description = \"When someone comments on your posts\"\n\n    @classmethod\n    def should_save(cls, notification):\n        # Look for existing unread notification with same actor and target\n        existing = Notification.objects.filter(\n            recipient=notification.recipient,\n            notification_type=notification.notification_type,\n            actor=notification.actor,\n            content_type_id=notification.content_type_id,\n            object_id=notification.object_id,\n            read__isnull=True,\n        ).first()\n\n        if existing:\n            # Update count in metadata\n            count = existing.metadata.get(\"count\", 1)\n            existing.metadata[\"count\"] = count + 1\n            existing.save()\n            return False  # Don't create new notification\n\n        # First notification of this type, so it should be saved\n        return True\n\n    def get_text(self, notification):\n        count = notification.metadata.get(\"count\", 1)\n        actor_name = notification.actor.get_full_name()\n\n        if count == 1:\n            return f\"{actor_name} commented on your post\"\n        else:\n            return f\"{actor_name} left {count} comments on your post\"\n```\n\nThe `should_save` method is called before saving each notification. Return `False` to prevent creating a new notification and instead update an existing one. This gives you complete control over grouping logic - you might group by time windows, actors, targets, or any other criteria.\n\n## Performance Considerations\n\n### Accessing `notification.target`\n\nWhile you can store any object into a notification's `target` field, it's usually not a great idea to use this field to dynamically create the notification's subject and text, as the `target` generic relationship can't be prefetched more than one level deep.\n\nIn other words, something like this will cause an N+1 query problem when you show a list of notifications in a table, for example:\n\n```python\nclass Comment(models.Model):\n    article = models.ForeignKey(Article, on_delete=models.CASCADE)\n    user = models.ForeignKey(User, on_delete=models.CASCADE)\n    comment_text = models.TextField()\n\n\n@register\nclass CommentNotificationType(NotificationType):\n    key = \"comment_notification\"\n    name = \"Comments\"\n    description = \"You received a comment\"\n\n    def get_text(self, notification):\n          actor_name = notification.actor.full_name\n          article = notification.target.article\n          comment_text = notification.target.comment.comment_text\n          return f'{actor_name} commented on your article \"{article.title}\": \"{comment_text}\"'\n\n```\n\nThe problem is `target.article`, which cannot be prefetched and thus causes another query for every notification. This is why it\u2019s better to store the subject, text and url in the notification itself, rather than relying on `target` dynamically.\n\n### Non-blocking email sending\n\nThe email channel (EmailChannel) will send real-time emails using Django\u2019s built-in `send_mail` method. This is a blocking function call, meaning that while a connection with the SMTP server is made and the email is sent off, the process that\u2019s sending the notification has to wait. This is not ideal, but easily solved by using something like [django-mailer](https://github.com/pinax/django-mailer/), which provides a queueing backend for `send_mail`. This means that sending email no longer is a blocking action\n\n## Example app\n\nAn example app is provided, which shows how to create a custom notification type, how to send a notification, it has a nice looking notification center with unread notifications as well as an archive of all read notifications, plus a settings view where you can manage notification preferences.\n\n```bash\ncd example\nuv run ./manage.py migrate\nuv run ./manage.py runserver\n```\n\nThen open http://127.0.0.1:8000/.\n\n## Development\n\n### Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/loopwerk/django-generic-notifications.git\ncd django-generic-notifications\n```\n\n### Testing\n\n```bash\n# Run all tests\nuv run pytest\n```\n\n### Code Quality\n\n```bash\n# Type checking\nuv run mypy .\n\n# Linting\nuv run ruff check .\n\n# Formatting\nuv run ruff format .\n```\n\n## License\n\nMIT License - see LICENSE file for details.\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "A flexible, multi-channel notification system for Django applications with built-in support for email digests, user preferences, and extensible delivery channels.",
    "version": "1.0.0",
    "project_urls": {
        "Homepage": "https://github.com/loopwerk/django-generic-notifications/",
        "Issues": "https://github.com/loopwerk/django-generic-notifications/issues",
        "Repository": "https://github.com/loopwerk/django-generic-notifications.git"
    },
    "split_keywords": [
        "django",
        " notifications",
        " email",
        " digest",
        " multi-channel"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "9ff3b75ea788936d04362fe6e8a618814ec18c257c51b080fb6b8bd0323fd4e6",
                "md5": "4d34be4eb584606ca5521bcd4d084636",
                "sha256": "c0e9a0b2f2ae2e3528f6ada3405af5f6a8d43fb1de3cdfae0daaa5e2c1d60465"
            },
            "downloads": -1,
            "filename": "django_generic_notifications-1.0.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "4d34be4eb584606ca5521bcd4d084636",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 19792,
            "upload_time": "2025-08-01T19:53:47",
            "upload_time_iso_8601": "2025-08-01T19:53:47.358844Z",
            "url": "https://files.pythonhosted.org/packages/9f/f3/b75ea788936d04362fe6e8a618814ec18c257c51b080fb6b8bd0323fd4e6/django_generic_notifications-1.0.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "149319613eb5cc576dbc99bf2f0e948ccbe1ea7d7efb4181527c07291ce11751",
                "md5": "608447609f4dbf0ee3312f5b7a5a46c4",
                "sha256": "8208a485c8a68c750566eb6ae212de7758bcc337cc70b140cfcfb72efc6e9635"
            },
            "downloads": -1,
            "filename": "django_generic_notifications-1.0.0.tar.gz",
            "has_sig": false,
            "md5_digest": "608447609f4dbf0ee3312f5b7a5a46c4",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 14292,
            "upload_time": "2025-08-01T19:53:48",
            "upload_time_iso_8601": "2025-08-01T19:53:48.608470Z",
            "url": "https://files.pythonhosted.org/packages/14/93/19613eb5cc576dbc99bf2f0e948ccbe1ea7d7efb4181527c07291ce11751/django_generic_notifications-1.0.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-01 19:53:48",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "loopwerk",
    "github_project": "django-generic-notifications",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "django-generic-notifications"
}
        
Elapsed time: 1.81518s