From 22c3241a52cf6b3e4ff45eeed9929ef52c1f63d0 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 11 Feb 2020 21:05:23 -0800 Subject: [PATCH] notification: Add developer documentation Reviewed-by: James Valleroy --- doc/dev/reference/index.rst | 1 + doc/dev/reference/notifications.rst | 7 ++ plinth/notification.py | 169 +++++++++++++++++++++++++++- 3 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 doc/dev/reference/notifications.rst diff --git a/doc/dev/reference/index.rst b/doc/dev/reference/index.rst index e8915e2a6..1a16ce3e5 100644 --- a/doc/dev/reference/index.rst +++ b/doc/dev/reference/index.rst @@ -19,5 +19,6 @@ and are updated when the API is updated. action_utils views forms + notifications .. automodule:: plinth.modules.ttrss diff --git a/doc/dev/reference/notifications.rst b/doc/dev/reference/notifications.rst new file mode 100644 index 000000000..5f6a422be --- /dev/null +++ b/doc/dev/reference/notifications.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +Notification +^^^^^^^^^^^^ + +.. autoclass:: plinth.notification.Notification + :members: diff --git a/plinth/notification.py b/plinth/notification.py index 525cf0e4a..89140b86e 100644 --- a/plinth/notification.py +++ b/plinth/notification.py @@ -21,7 +21,139 @@ logger = logging.getLogger(__name__) class Notification(models.StoredNotification): - """Wrapper over Notification model to provide the API.""" + """API to create persistent global notifications to users. + + The number of notifications is shown on the top navigation bar of every + page that the user loads. On clicking the notification icon, a full list of + notifications is shown as a drop down that works on both on Desktop and + Mobile interfaces. + + The notification API provides the following functionality. + + * Store the notifications on disk and persist them across restarts. + + * Update a notification for any of its stored details. + + * Show icon and name of the app that shows the notification. + + * Present the notification title and message. + + * Present actions that user can perform on the notification. + + * Allow for full customization of content and actions of the notification. + This allows for use cases such as showing progress bar. This is done by + allowing a custom template to be used for rendering. + + * Internationalization and localization of displayed notification. + + * Allow the notification to be dismissed and resurrected after dismissal. + + * Store arbitrary data as a dictionary along with the notification. This + data is used for formatting localized strings and for rendering the + custom template. + + * Limit a notification to a group of users or to a single user. + + * Retrieve a notification using specific string identifier. + + * Iterate all the notifications. + + * Filter notifications by app that created them, by whether the + notification meant for a particular user and by whether a notification is + dismissed or not. + + * Provide a convenient way to obtain the context for rendering templates + that intend to show notifications. + + This class is a wrapper over Notification model to provide a simplified + API. So, all of the Django model API is available on the class. The + following fields are present in the model: + + 'id' is a unique string identifier for the notification and acts as the + primary key for the stored database table. + + 'app_id' is the unique ID of the app showing the notification. + + 'severity' is a string indicating the severity of the notification which + can take one of the following values: 'exception', 'error', 'warning', + 'info' and 'debug'. + + 'title' is a user visible string that is the main heading of the + notification. 'title' may be used to show a summary of notifications even + 'body_template' is used for customization. So, it is mandatory. The user + who requested the operation that caused the notification may be different + from the user that sees the notification. Each user wishes to see the + notification in their own locale. This string is stored to the disk as it + is. It is translated and then formatted with 'data' dictionary just before + showing it to the user. So, it must be marked for translation with + (u)gettext_noop but must not be translated using (u)gettext or similar + methods. + + 'message' is a user visible string that is the main body of the + notification. It is formatted as a single paragraph. It is not used and + must not be provided when 'body_template' is provided. It must be marked + for translation but not translated similar to 'title'. + + 'actions' is a list of dictionaries containing information about what + operations, such as dismiss, that the user may perform on the notification. + When not provided, no actions are shown and the notification can't be + dismissed. It is not used and must not be provided when 'body_template' is + provided. When 'body_template' is provided, the template is expected to + list its actions inside the template. + + A example of 'actions' structure is as follows: [{'type': 'dismiss'}, + {'type': 'link', 'class': 'warning', 'text': 'Cleanup', 'url': + 'storage:index'}]. The 'type' parameter can be 'dismiss' or 'link'. When + 'dismiss' is the type, no other information is necessary and a simple + action to dismiss the notification is provided. When the 'type' is 'link', + a button is shown with label provided by 'text'. The 'text' property is + must be marked for translation but not translated similar to 'title'. When + the button is clicked, it will direct the user to the Django URL indicated + by 'url'. 'class' is used for CSS styling of the button. It can taken on + any of the following values; 'primary', 'default', 'warning', 'danger', + 'success', 'info'. + + 'body_template' is the name of the Django template to use instead of + 'title', 'message' and 'actions'. It enables the the notification creator + to go beyond the simple presentation offered by default and include rich + content such as progress bars and forms inside a notification. The template + itself is expected to contain a meaningful title, body and operations to be + performed on the notification. It is an error to provide 'body_template' + and any one of 'message' and 'actions' at the same time. 'title' may still + be used when notifications are presented in other situations. The template + is rendered with 'data' as it's context. So, any key/values to be included + in the context must become part of the 'data'. + + 'data' is a custom dictionary that is serialized and stored along with the + notification. It is has three purposes; to format the translated strings + before showing to the user, to be used as context for rendering a custom + template and to store any extra data associated with the notification on + behalf of the it's creator for future reference. Any string value in the + dictionary, even in deeper levels, if prefixed by 'translate:' will be + translated before being used. + + 'created_time' is a date/time field that is automatically populated when + the notification is created. + + 'last_update_time' is a date/time field that is automatically modified when + the notification is updated with newer properties, including changes to the + 'dismissed' property. + + 'user' is the username of the account to which this notification should be + shown to. When not provided or set to None, it means that the notification + is meant for all the users (subject to 'group' restriction). + + 'group' is the group of users to which this notification should be shown + to. When not provided or set to None, it means that the notification is + meant for all the users (subject to 'user' restriction). If a user in the + group dismisses the notification, it is no longer shown to other users. To + let each user see the notification, create multiple notifications each + restricted to a single user account instead. + + 'dismissed' is a boolean flag that indicates whether the notification has + been dismissed by the user. + + """ class Meta: # pylint: disable=too-few-public-methods """Meta properties of the Notification model.""" proxy = True @@ -35,7 +167,12 @@ class Notification(models.StoredNotification): return severities['info'] def dismiss(self, should_dismiss=True): - """Mark the notification as read or unread.""" + """Mark the notification as read or unread. + + If 'should_dismiss' is True, the notification is dismissed else the + notification is resurrected even after being dismissed. + + """ self.dismissed = should_dismiss super().save() @@ -81,13 +218,19 @@ class Notification(models.StoredNotification): @staticmethod def update_or_create(**kwargs): - """Update a notification or create one if necessary.""" + """Update a notification or create one if necessary. + + 'id' field will be used to retrieve the notification and all other + fields values will be updated after retrieval. If not match is found, a + new notification is created and returned. + + """ id = kwargs.pop('id') return Notification.objects.update_or_create(defaults=kwargs, id=id)[0] @staticmethod def get(key): # pylint: disable=redefined-builtin - """Return a notification with matching ID.""" + """Return a notification object with a matching ID.""" # pylint: disable=no-member try: return Notification.objects.get(pk=key) @@ -96,7 +239,23 @@ class Notification(models.StoredNotification): @staticmethod def list(key=None, app_id=None, user=None, dismissed=False): - """Return a list of notifications for a user.""" + """Return a list of notifications for a user. + + 'key' if provided, return only notifications that match this value as + their 'id'. + + 'app_id' if provided, return only notifications that match this + app_id. + + 'user' must be a Django request.user structure. If provided, only + notifications that are meant for this user account specifically or + meant for any of groups that this user belongs to will be returned. + + 'dismissed' is the a boolean flag or None. If not None, only + notifications with the matching in their 'dismissed' field will be + returned. + + """ filters = [] if key: filters.append(Q(id=key))