Skip to content

Commit 31e04c9

Browse files
samuelallan72farhaanbukhsh
authored andcommitted
docs: add initial documentation for notifications
- Document the types used in COURSE_NOTIFICATION_TYPES and COURSE_NOTIFICATION_APPS. - Port the wiki page on creating a new notification to the docs here. - Add some miscellaneous docs and placeholder TODO notes. - Add a data flow diagram. - Add a short getting started guide for operators (mainly for getting email notifications working). Private-ref: https://tasks.opencraft.com/browse/BB-10065
1 parent aef9e53 commit 31e04c9

File tree

8 files changed

+272
-6
lines changed

8 files changed

+272
-6
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Notifications
2+
3+
Functionality for notifications on Open edX.
4+
5+
See the [./docs/](./docs/) directory for docs.

openedx/core/djangoapps/notifications/base_notification.py

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Base setup for Notification Apps and Types.
33
"""
4+
from typing import Any, Literal, TypedDict, NotRequired
5+
46
from django.utils.translation import gettext_lazy as _
57

68
from .email_notifications import EmailCadence
@@ -11,6 +13,54 @@
1113

1214
FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE = 'filter_audit_expired_users_with_no_role'
1315

16+
17+
class NotificationType(TypedDict):
18+
"""
19+
Define the fields for values in COURSE_NOTIFICATION_TYPES
20+
"""
21+
# The notification app associated with this notification.
22+
# Must be a key in COURSE_NOTIFICATION_APPS.
23+
notification_app: str
24+
# Unique identifier for this notification type.
25+
name: str
26+
# Mark this as a core notification.
27+
# When True, user preferences are taken from the notification app's `core_*` configuration,
28+
# overriding the `web`, `email`, `push`, `email_cadence`, and `non_editable` attributes set here.
29+
is_core: bool
30+
# Template string for notification content (see ./docs/templates.md).
31+
# Wrap in gettext_lazy (_) for translation support.
32+
content_template: str
33+
# A map of variable names that can be used in the template, along with their descriptions.
34+
# The values for these variables are passed to the templates when generating the notification.
35+
# NOTE: this field is for documentation purposes only; it is not used.
36+
content_context: dict[str, Any]
37+
# Template used when delivering notifications via email.
38+
email_template: str
39+
filters: list[str]
40+
41+
# All fields below are required unless `is_core` is True.
42+
# Core notifications take this config from the associated notification app instead (and ignore anything set here).
43+
44+
# Set to True to enable delivery on web.
45+
web: NotRequired[bool]
46+
# Set to True to enable delivery via email.
47+
email: NotRequired[bool]
48+
# Set to True to enable delivery via push notifications.
49+
# NOTE: push notifications are not implemented yet
50+
push: NotRequired[bool]
51+
# How often email notifications are sent.
52+
email_cadence: NotRequired[Literal[
53+
EmailCadence.DAILY, EmailCadence.WEEKLY, EmailCadence.IMMEDIATELY, EmailCadence.NEVER
54+
]]
55+
# Items in the list represent delivery channels
56+
# where the user is blocked from changing from what is defined for the notification here
57+
# (see `web`, `email`, and `push` above).
58+
non_editable: NotRequired[list[Literal["web", "email", "push"]]]
59+
# Descriptive information about the notification.
60+
info: NotRequired[str]
61+
62+
63+
# For help defining new notifications, see ./docs/creating_a_new_notification_guide.md
1464
COURSE_NOTIFICATION_TYPES = {
1565
'new_comment_on_response': {
1666
'notification_app': 'discussion',
@@ -250,7 +300,42 @@
250300
},
251301
}
252302

253-
COURSE_NOTIFICATION_APPS = {
303+
304+
class NotificationApp(TypedDict):
305+
"""
306+
Define the fields for values in COURSE_NOTIFICATION_APPS
307+
308+
An instance of this type describes a notification app,
309+
which is a way of grouping configuration of types of notifications for users.
310+
311+
Each notification type defined in COURSE_NOTIFICATION_TYPES also references an app.
312+
313+
Each notification type can also be optionally defined as a core notification.
314+
In this case, the delivery preferences for that notification are taken
315+
from the `core_*` fields of the associated notification app.
316+
"""
317+
# Set to True to enable this app and linked notification types.
318+
enabled: bool
319+
# Description to be displayed about core notifications for this app.
320+
# This string should be wrapped in the gettext_lazy function (imported as `_`) to support translation.
321+
core_info: str
322+
# Set to True to enable delivery for associated core notifications on web.
323+
core_web: bool
324+
# Set to True to enable delivery for associated core notifications via emails.
325+
core_email: bool
326+
# Set to True to enable delivery for associated core notifications via push notifications.
327+
# NOTE: push notifications are not implemented yet
328+
core_push: bool
329+
# How often email notifications are sent for associated core notifications.
330+
core_email_cadence: Literal[EmailCadence.DAILY, EmailCadence.WEEKLY, EmailCadence.IMMEDIATELY, EmailCadence.NEVER]
331+
# Items in the list represent core notification delivery channels
332+
# where the user is blocked from changing from what is defined for the app here
333+
# (see `core_web`, `core_email`, and `core_push` above).
334+
non_editable: list[Literal["web", "email", "push"]]
335+
336+
337+
# For help defining new notifications and notification apps, see ./docs/creating_a_new_notification_guide.md
338+
COURSE_NOTIFICATION_APPS: dict[str, NotificationApp] = {
254339
'discussion': {
255340
'enabled': True,
256341
'core_info': _('Notifications for responses and comments on your posts, and the ones you’re '
@@ -391,18 +476,22 @@ class NotificationTypeManager:
391476
def __init__(self):
392477
self.notification_types = COURSE_NOTIFICATION_TYPES
393478

394-
def get_notification_types_by_app(self, notification_app):
479+
def get_notification_types_by_app(self, notification_app: str):
395480
"""
396-
Returns notification types for the given notification app.
481+
Returns notification types for the given notification app name.
397482
"""
398483
return [
399484
notification_type.copy() for _, notification_type in self.notification_types.items()
400485
if notification_type.get('notification_app', None) == notification_app
401486
]
402487

403-
def get_core_and_non_core_notification_types(self, notification_app):
488+
def get_core_and_non_core_notification_types(
489+
self, notification_app: str
490+
) -> tuple[NotificationType, NotificationType]:
404491
"""
405-
Returns core notification types for the given app name.
492+
Returns notification types for the given app name, split by core and non core.
493+
494+
Return type is a tuple of (core_notification_types, non_core_notification_types).
406495
"""
407496
notification_types = self.get_notification_types_by_app(notification_app)
408497
core_notification_types = []
@@ -498,7 +587,7 @@ def get_notification_app_preferences(self, email_opt_out=False):
498587
return course_notification_preference_config
499588

500589

501-
def get_notification_content(notification_type, context):
590+
def get_notification_content(notification_type: str, context: dict[str, Any]):
502591
"""
503592
Returns notification content for the given notification type with provided context.
504593
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
## Data flow
3+
4+
Below is a diagram of how data flows through the notification system.
5+
6+
![data flow diagram](./data-flow.jpg)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Creating a new notification
2+
3+
This documentation provides instructions to developers on how to add new notifications to the existing notification system.
4+
5+
## Overview of terms
6+
7+
* Type: every notification a user sees has a defined type. This type describes the notification's behaviour and template text.
8+
* App: each notification type is associated with a notification app. A notification app is simply a way to group notifications, and to provide a mechanism for shared behaviour.
9+
* Core notification: a notification type can be labelled as a core notification. In this case, the behaviour is managed at the app level, not at the level of the individual notification type.
10+
11+
## Defining a notification
12+
13+
The configuration consists of notification types and notification apps. Follow the steps below to define a new notification type.
14+
15+
### Step 1: Define the Notification App
16+
17+
The first step to defining a new notification is deciding on an app to associate it with. Either choose an existing one or create a new one in `COURSE_NOTIFICATION_APPS` in [base_notification.py](../base_notification.py). For example, here is an app named "discussion":
18+
19+
```python
20+
COURSE_NOTIFICATION_APPS = {
21+
'discussion': {
22+
'enabled': True,
23+
'core_info': '',
24+
'core_web': True,
25+
'core_email': True,
26+
'core_push': True,
27+
'non_editable': [],
28+
'core_email_cadence': 'weekly'
29+
}
30+
}
31+
```
32+
33+
The app name (the key) can be any name you wish to add but ideally it should represent existing Django apps in the project.
34+
For an explanation of the available fields, see `NotificationApp` in [base_notification.py](../base_notification.py).
35+
36+
### **Step 2: Define the Notification Type**
37+
38+
Now you can define the notification type itself.
39+
To do this, add a new entry to `COURSE_NOTIFICATION_TYPES` in [base_notification.py](../base_notification.py).
40+
For example, here is a notification defined for a new response to a discussion forum post, associated with the "discussion" app example from the previous step:
41+
42+
```python
43+
COURSE_NOTIFICATION_TYPES = {
44+
'new_response': {
45+
'notification_app': 'discussion',
46+
'name': 'new_response',
47+
'is_core': False,
48+
'web': True,
49+
'email': True,
50+
'push': True,
51+
'info': 'Response on post',
52+
'non_editable': [],
53+
'content_template': _('<{p}><{strong}>{replier_name}</{strong}> responded to your post <{strong}>{post_title}</{strong}></{p}>'),
54+
'content_context': {
55+
'post_title': 'Post title',
56+
'replier_name': 'replier name',
57+
},
58+
'email_template': '',
59+
},
60+
}
61+
```
62+
63+
For an explanation of the available fields, see `NotificationType` in [base_notification.py](../base_notification.py).
64+
65+
### Step 3: Update the version in the model file
66+
67+
Newly added types are only usable once you have updated the value of `COURSE_NOTIFICATION_CONFIG_VERSION` in [notifications/models.py](../models.py).
68+
This constant is used to track changes in notification configuration, and whenever this version is updated preferences of users are also updated with newly available types.
69+
To update it, increment the value by 1.
70+
71+
Adding new notification types without this step will have no effect.
72+
You don't need to update the constant for changes that are not stored in database (eg. templates).
73+
74+
Now the notification type is defined and ready to use!
75+
The next section details how to use this notification type to create and send a notification.
76+
77+
## Sending a notification
78+
79+
To send a notification, you need to send the `USER_NOTIFICATION_REQUESTED` signal with an instance of `UserNotificationData` containing information about the notification to send.
80+
81+
Below is an example function to build and send the `new_response` notification type from earlier.
82+
83+
from openedx_events.learning.signals import USER_NOTIFICATION_REQUESTED
84+
from openedx_events.learning.data import UserNotificationData
85+
86+
```python
87+
def send_new_response_notification(user_ids, course, thread, replier_user):
88+
notification_data = UserNotificationData(
89+
user_ids=user_ids,
90+
notification_type="new_response",
91+
content_url=f"/{course.id}/posts/{thread.id}",
92+
app_name='discussion',
93+
course_key=course.id,
94+
context={
95+
'post_title': thread.title,
96+
'replier_name': replier_user.username,
97+
},
98+
)
99+
USER_NOTIFICATION_REQUESTED.send_event(notification_data=notification_data)
100+
```
101+
102+
Explanation of the parameters for `UserNotificationData`:
103+
104+
| Name | Type | Description |
105+
| :---- | :---- | :---- |
106+
| `user_ids` | `list[int]` | List of user IDs to send the notification to. |
107+
| `notification_type` | `str` | The type of notification to send. This must be a key of `COURSE_NOTIFICATION_TYPES`. |
108+
| `content_url` | `str` | Url the user will navigate to if they click on the notification. |
109+
| `app_name` | `str` | The app this notification is associated with. This must be a key of `COURSE_NOTIFICATION_APPS`. |
110+
| `course_key` | `CourseKey` | The course that this notification will be associated with. |
111+
| `context` | `dict[str, str]` | Context variables and values to pass to the notification content template. Keys are the variable names defined in the notification type. |
112+
113+
That's it! You have implemented the code to send a new user notification using the `USER_NOTIFICATION_REQUESTED` signal.
114+
115+
## Grouping notifications
116+
117+
For some notification types, the volume for a learner can be huge and can cause annoyance.
118+
For example, if a learner creates a post, and other learners and staff members start adding responses to his post, if for each comment, we add a response, it could result in dozens of notifications.
119+
To avoid these scenarios, we have implemented a feature that allows grouping more than one similar notifications into a single notification.
120+
Steps to group a notification:
121+
122+
1. Enable grouping waffle flag `notifications.enable_notification_grouping`.
123+
2. Add `group_by_id` in context before sending the `USER_NOTIFICATION_REQUESTED` event (see [discussions_notifications.py](../../../../../lms/djangoapps/discussion/rest_api/discussions_notifications.py), and search for `group_by_id` for an example).
124+
3. Implement a grouper class to modify content_context (see [grouping_notifications.py](../grouping_notifications.py) for an example).
125+
126+
## Legal
127+
128+
When adding a new notification type, you will need a Privacy threshold assessment done by legal.
129+
130+
## Troubleshooting
131+
132+
If you have followed the above steps and notifications are still not working, check if the `notifications.enable_notifications` waffle flag is enabled.
1.58 MB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Getting started with notifications
2+
3+
1. You will need to configure `NOTIFICATIONS_DEFAULT_FROM_EMAIL` to send email notifications.
4+
2. Daily and weekly digest emails require the respective management commands to be run on a daily and weekly basis:
5+
- daily: `manage.py lms send_email_digest Daily`
6+
- weekly: `manage.py lms send_email_digest Weekly`
7+
8+
Example crontab entries:
9+
10+
```
11+
0 22 * * * ./manage.py lms send_email_digest Daily
12+
0 22 * * SUN ./manage.py lms send_email_digest Weekly
13+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# How to enable the notification tray
2+
3+
On the front end, the notification tray needs to be enabled to simplify the user experience.
4+
Users can click the bell icon in the header to access the notification tray, which will display notifications from the apps listed above.
5+
For detailed steps please view the following document: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/5319491585/How+to+Enable+Notification+Tray
6+
7+
And explicit implementation of the notification tray on the Learning Dashboard can be viewed in the following document: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/5321916443/Notification+Tray+Implementation+in+Learner+Dashboard
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
# Notification template spec
3+
4+
TODO: this needs expanding
5+
6+
- Use `_(...)` to mark the template as translatable.
7+
- Be sure to add content in `<p> ... </p>` tags. Use `<strong>` to make words bold.
8+
- Note that only `<p>` and `<strong>` are supported.
9+
10+
TODO: show an example
11+
12+
# Notification grouped content template spec
13+
14+
TODO

0 commit comments

Comments
 (0)