diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst new file mode 100644 index 00000000..6ee3062b --- /dev/null +++ b/docs/developer/extending.rst @@ -0,0 +1,363 @@ +Extending openwisp-notifications +================================ + +.. include:: ../partials/developer-docs.rst + +One of the core values of the OpenWISP project is :ref:`Software +Reusability `, for this reason OpenWISP +Notifications provides a set of base classes which can be imported, +extended and reused to create derivative apps. + +In order to implement your custom version of *openwisp-notifications*, you +need to perform the steps described in the rest of this section. + +When in doubt, the code in `test project +`_ +and `sample_notifications +`_ +will guide you in the correct direction: just replicate and adapt that +code to get a basic derivative of *openwisp-notifications* working. + +.. important:: + + If you plan on using a customized version of this module, we suggest + to start with it since the beginning, because migrating your data from + the default module to your extended version may be time consuming. + +.. contents:: **Table of Contents**: + :depth: 2 + :local: + +1. Initialize your custom module +-------------------------------- + +The first thing you need to do in order to extend *openwisp-notifications* +is create a new django app which will contain your custom version of that +*openwisp-notifications* app. + +A django app is nothing more than a `python package +`_ (a directory +of python scripts), in the following examples we'll call this django app +as ``mynotifications`` but you can name it how you want: + +.. code-block:: shell + + django-admin startapp mynotifications + +Keep in mind that the command mentioned above must be called from a +directory which is available in your `PYTHON_PATH +`_ so that +you can then import the result into your project. + +Now you need to add ``mynotifications`` to ``INSTALLED_APPS`` in your +``settings.py``, ensuring also that ``openwisp_notifications`` has been +removed: + +.. code-block:: python + + INSTALLED_APPS = [ + # ... other apps ... + # 'openwisp_notifications', <-- comment out or delete this line + "mynotifications", + ] + +For more information about how to work with django projects and django +apps, please refer to the `django documentation +`_. + +2. Install ``openwisp-notifications`` +------------------------------------- + +Install (and add to the requirement of your project) +*openwisp-notifications*: + +.. code-block:: shell + + pip install -U https://github.com/openwisp/openwisp-notifications/tarball/master + +3. Add ``EXTENDED_APPS`` +------------------------ + +Add the following to your ``settings.py``: + +.. code-block:: python + + EXTENDED_APPS = ["openwisp_notifications"] + +4. Add ``openwisp_utils.staticfiles.DependencyFinder`` +------------------------------------------------------ + +Add ``openwisp_utils.staticfiles.DependencyFinder`` to +``STATICFILES_FINDERS`` in your ``settings.py``: + +.. code-block:: python + + STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "openwisp_utils.staticfiles.DependencyFinder", + ] + +5. Add ``openwisp_utils.loaders.DependencyLoader`` +-------------------------------------------------- + +Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your +``settings.py``: + +.. code-block:: python + + TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "OPTIONS": { + "loaders": [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + "openwisp_utils.loaders.DependencyLoader", + ], + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + } + ] + +6. Inherit the AppConfig class +------------------------------ + +Please refer to the following files in the sample app of the test project: + +- `sample_notifications/__init__.py + `_. +- `sample_notifications/apps.py + `_. + +For more information regarding the concept of ``AppConfig`` please refer +to the `"Applications" section in the django documentation +`_. + +7. Create your custom models +---------------------------- + +For the purpose of showing an example, we added a simple "details" field +to the `models of the sample app in the test project +`_. + +You can add fields in a similar way in your ``models.py`` file. + +**Note**: For doubts regarding how to use, extend or develop models please +refer to the `"Models" section in the django documentation +`_. + +8. Add swapper configurations +----------------------------- + +Add the following to your ``settings.py``: + +.. code-block:: python + + # Setting models for swapper module + OPENWISP_NOTIFICATIONS_NOTIFICATION_MODEL = "mynotifications.Notification" + OPENWISP_NOTIFICATIONS_NOTIFICATIONSETTING_MODEL = ( + "mynotifications.NotificationSetting" + ) + OPENWISP_NOTIFICATIONS_IGNOREOBJECTNOTIFICATION_MODEL = ( + "mynotifications.IgnoreObjectNotification" + ) + +9. Create database migrations +----------------------------- + +Create and apply database migrations: + +.. code-block:: + + ./manage.py makemigrations + ./manage.py migrate + +For more information, refer to the `"Migrations" section in the django +documentation +`_. + +10. Create your custom admin +---------------------------- + +Refer to the `admin.py file of the sample app +`_. + +To introduce changes to the admin, you can do it in two main ways which +are described below. + +**Note**: For more information regarding how the django admin works, or +how it can be customized, please refer to `"The django admin site" section +in the django documentation +`_. + +1. Monkey patching +~~~~~~~~~~~~~~~~~~ + +If the changes you need to add are relatively small, you can resort to +monkey patching. + +For example: + +.. code-block:: python + + from openwisp_notifications.admin import NotificationSettingInline + + NotificationSettingInline.list_display.insert(1, "my_custom_field") + NotificationSettingInline.ordering = ["-my_custom_field"] + +2. Inheriting admin classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to introduce significant changes and/or you don't want to +resort to monkey patching, you can proceed as follows: + +.. code-block:: python + + from django.contrib import admin + from openwisp_notifications.admin import ( + NotificationSettingInline as BaseNotificationSettingInline, + ) + from openwisp_notifications.swapper import load_model + + NotificationSetting = load_model("NotificationSetting") + + admin.site.unregister(NotificationSettingAdmin) + admin.site.unregister(NotificationSettingInline) + + + @admin.register(NotificationSetting) + class NotificationSettingInline(BaseNotificationSettingInline): + # add your changes here + pass + +11. Create root URL configuration +--------------------------------- + +Please refer to the `urls.py +`_ +file in the test project. + +For more information about URL configuration in django, please refer to +the `"URL dispatcher" section in the django documentation +`_. + +12. Create root routing configuration +------------------------------------- + +Please refer to the `routing.py +`_ +file in the test project. + +For more information about URL configuration in django, please refer to +the `"Routing" section in the Channels documentation +`_. + +13. Create celery.py +-------------------- + +Please refer to the `celery.py +`_ +file in the test project. + +For more information about the usage of celery in django, please refer to +the `"First steps with Django" section in the celery documentation +`_. + +14. Import Celery Tasks +----------------------- + +Add the following in your settings.py to import celery tasks from +``openwisp_notifications`` app. + +.. code-block:: python + + CELERY_IMPORTS = ("openwisp_notifications.tasks",) + +15. Register Template Tags +-------------------------- + +If you need to use template tags of *openwisp_notifications*, you will +need to register as shown in `"templatetags/notification_tags.py" of +sample_notifications +`_. + +For more information about template tags in django, please refer to the +`"Custom template tags and filters" section in the django documentation +`_. + +16. Register Notification Types +------------------------------- + +You can register notification types as shown in the :ref:`section for +registering notification types `. + +A reference for registering a notification type is also provided in +`sample_notifications/apps.py +`_. +The registered notification type of ``sample_notifications`` app is used +for creating notifications when an object of ``TestApp`` model is created. +You can use `sample_notifications/models.py +`_ +as reference for your implementation. + +17. Import the automated tests +------------------------------ + +When developing a custom application based on this module, it's a good +idea to import and run the base tests too, so that you can be sure the +changes you're introducing are not breaking some of the existing feature +of openwisp-notifications. + +In case you need to add breaking changes, you can overwrite the tests +defined in the base classes to test your own behavior. + +See the `tests of the sample_notifications +`_ +to find out how to do this. + +**Note**: Some tests will fail if ``templatetags`` and ``admin/base.html`` +are not configured properly. See preceeding sections to configure them +properly. + +Other base classes that can be inherited and extended +----------------------------------------------------- + +The following steps are not required and are intended for more advanced +customization. + +API views +~~~~~~~~~ + +The API view classes can be extended into other django applications as +well. Note that it is not required for extending openwisp-notifications to +your app and this change is required only if you plan to make changes to +the API views. + +Create a view file as done in `sample_notifications/views.py +`_ + +For more information regarding Django REST Framework API views, please +refer to the `"Generic views" section in the Django REST Framework +documentation +`_. + +Web Socket Consumers +~~~~~~~~~~~~~~~~~~~~ + +The Web Socket Consumer classes can be extended into other django +applications as well. Note that it is not required for extending +openwisp-notifications to your app and this change is required only if you +plan to make changes to the consumers. + +Create a consumer file as done in `sample_notifications/consumers.py +`_ + +For more information regarding Channels' Consumers, please refer to the +`"Consumers" section in the Channels documentation +`_. diff --git a/docs/developer/index.rst b/docs/developer/index.rst new file mode 100644 index 00000000..99915fc9 --- /dev/null +++ b/docs/developer/index.rst @@ -0,0 +1,16 @@ +Developer Docs +============== + +.. include:: ../partials/developer-docs.rst + +.. toctree:: + :maxdepth: 2 + + ./installation.rst + ./utils.rst + ./extending.rst + +Other useful resources: + + - :doc:`../user/rest-api` + - :doc:`../user/settings` diff --git a/docs/developer/installation.rst b/docs/developer/installation.rst new file mode 100644 index 00000000..917605ec --- /dev/null +++ b/docs/developer/installation.rst @@ -0,0 +1,132 @@ +Developer Installation Instructions +=================================== + +.. include:: ../partials/developer-docs.rst + +.. contents:: **Table of Contents**: + :depth: 2 + :local: + +Installing for Development +-------------------------- + +Install the system dependencies: + +.. code-block:: shell + + sudo apt install sqlite3 libsqlite3-dev openssl libssl-dev + +Fork and clone the forked repository: + +.. code-block:: shell + + git clone git://github.com//openwisp-notifications + +Navigate into the cloned repository: + +.. code-block:: shell + + cd openwisp-notifications/ + +Launch Redis: + +.. code-block:: shell + + docker-compose up -d redis + +Setup and activate a virtual-environment (we'll be using `virtualenv +`_): + +.. code-block:: shell + + python -m virtualenv env + source env/bin/activate + +Make sure that your base python packages are up to date before moving to +the next step: + +.. code-block:: shell + + pip install -U pip wheel setuptools + +Install development dependencies: + +.. code-block:: shell + + pip install -e . + pip install -r requirements-test.txt + sudo npm install -g jshint stylelint + +Create database: + +.. code-block:: shell + + cd tests/ + ./manage.py migrate + ./manage.py createsuperuser + +Launch celery worker (for background jobs): + +.. code-block:: shell + + celery -A openwisp2 worker -l info + +Launch development server: + +.. code-block:: shell + + ./manage.py runserver + +You can access the admin interface at ``http://127.0.0.1:8000/admin/``. + +Run tests with: + +.. code-block:: shell + + # standard tests + ./runtests.py + + # If you running tests on PROD environment + ./runtests.py --exclude skip_prod + + # tests for the sample app + SAMPLE_APP=1 ./runtests.py + +When running the last line of the previous example, the environment +variable ``SAMPLE_APP`` activates the sample app in ``/tests/openwisp2/`` +which is a simple django app that extends ``openwisp-notifications`` with +the sole purpose of testing its extensibility, for more information +regarding this concept, read the following section. + +Run quality assurance tests with: + +.. code-block:: shell + + ./run-qa-checks + +Alternative Sources +------------------- + +Pypi +~~~~ + +To install the latest stable version from pypi: + +.. code-block:: shell + + pip install openwisp-notifications + +Github +~~~~~~ + +To install the latest development version tarball via HTTPs: + +.. code-block:: shell + + pip install https://github.com/openwisp/openwisp-notifications/tarball/master + +Alternatively you can use the git protocol: + +.. code-block:: shell + + pip install -e git+git://github.com/openwisp/openwisp-notifications#egg=openwisp_notifications diff --git a/docs/developer/utils.rst b/docs/developer/utils.rst new file mode 100644 index 00000000..3e09a637 --- /dev/null +++ b/docs/developer/utils.rst @@ -0,0 +1,135 @@ +Code Utilities +============== + +.. include:: ../partials/developer-docs.rst + +.. contents:: **Table of Contents**: + :depth: 2 + :local: + +.. _dd: + +Registering / Unregistering Notification Types +---------------------------------------------- + +OpenWISP Notifications provides registering and unregistering +notifications through utility functions +``openwisp_notifications.types.register_notification_type`` and +``openwisp_notifications.types.unregister_notification_type``. Using these +functions you can register or unregister notification types from your +code. + +.. important:: + + It is recommended that all notification types are registered or + unregistered in ``ready`` method of your Django application's + ``AppConfig``. + +.. _notifications_register_type: + +``register_notification_type`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This function is used to register a new notification type from your code. + +Syntax: + +.. code-block:: python + + register_notification_type(type_name, type_config, models) + +============= =========================================================== +**Parameter** **Description** +type_name A ``str`` defining name of the notification type. +type_config A ``dict`` defining configuration of the notification type. +models An optional ``list`` of models that can be associated with + the notification type. +============= =========================================================== + +An example usage has been shown below. + +.. code-block:: python + + from openwisp_notifications.types import register_notification_type + from django.contrib.auth import get_user_model + + User = get_user_model() + + # Define configuration of your notification type + custom_type = { + "level": "info", + "verb": "added", + "verbose_name": "device added", + "message": "[{notification.target}]({notification.target_link}) was {notification.verb} at {notification.timestamp}", + "email_subject": "[{site.name}] A device has been added", + "web_notification": True, + "email_notification": True, + # static URL for the actor object + "actor": "https://openwisp.org/admin/config/device", + # URL generation using callable for target object + "target": "mymodule.target_object_link", + } + + # Register your custom notification type + register_notification_type("custom_type", custom_type, models=[User]) + +It will raise ``ImproperlyConfigured`` exception if a notification type is +already registered with same name(not to be confused with +``verbose_name``). + +.. note:: + + You can use ``site`` and ``notification`` variables while defining + ``message`` and ``email_subject`` configuration of notification type. + They refer to objects of ``django.contrib.sites.models.Site`` and + ``openwisp_notifications.models.Notification`` respectively. This + allows you to use any of their attributes in your configuration. + Similarly to ``message_template``, ``message`` property can also be + formatted using markdown. + +``unregister_notification_type`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This function is used to unregister a notification type from anywhere in +your code. + +Syntax: + +.. code-block:: python + + unregister_notification_type(type_name) + +============= ================================================= +**Parameter** **Description** +type_name A ``str`` defining name of the notification type. +============= ================================================= + +An example usage is shown below. + +.. code-block:: python + + from openwisp_notifications.types import unregister_notification_type + + # Unregister previously registered notification type + unregister_notification_type("custom type") + +It will raise ``ImproperlyConfigured`` exception if the concerned +notification type is not registered. + +Exceptions +~~~~~~~~~~ + +``NotificationRenderException`` ++++++++++++++++++++++++++++++++ + +.. code-block:: python + + openwisp_notifications.exceptions.NotificationRenderException + +Raised when notification properties(``email`` or ``message``) cannot be +rendered from concerned *notification type*. It sub-classes ``Exception`` +class. + +It can be raised due to accessing non-existing keys like missing related +objects in ``email`` or ``message`` setting of concerned *notification +type*. diff --git a/docs/images/architecture-v2-openwisp-notifications.png b/docs/images/architecture-v2-openwisp-notifications.png new file mode 100644 index 00000000..4aa5c0bd Binary files /dev/null and b/docs/images/architecture-v2-openwisp-notifications.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..db07b475 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,56 @@ +Notifications +============= + +.. seealso:: + + **Source code**: `github.com/openwisp/openwisp-notifications + `_. + +OpenWISP Notifications is a versatile system designed to deliver email and +web notifications. Its primary function is to enable other OpenWISP +modules to alert users about significant events occurring within their +network. By seamlessly integrating with various OpenWISP components, it +ensures users are promptly informed about critical updates and changes. +This enhances the overall user experience by keeping network +administrators aware and responsive to important developments. + +For a comprehensive overview of features, please refer to the +:doc:`user/intro` page. + +The following diagram illustrates the role of the Notifications module +within the OpenWISP architecture. + +.. figure:: images/architecture-v2-openwisp-notifications.png + :target: ../_images/architecture-v2-openwisp-notifications.png + :align: center + :alt: OpenWISP Architecture: Notifications module + + **OpenWISP Architecture: highlighted notifications module** + +.. important:: + + For an enhanced viewing experience, open the image above in a new + browser tab. + + Refer to :doc:`/general/architecture` for more information. + +.. toctree:: + :caption: Notifications Usage Docs + :maxdepth: 1 + + ./user/intro.rst + ./user/notification-types.rst + ./user/sending-notifications.rst + ./user/web-email-notifications.rst + ./user/notification-preferences.rst + ./user/scheduled-deletion-of-notifications.rst + ./user/rest-api.rst + ./user/settings.rst + ./user/notification-cache.rst + ./user/management-commands.rst + +.. toctree:: + :caption: Notifications Developer Docs + :maxdepth: 2 + + Developer Docs Index diff --git a/docs/partials/developer-docs.rst b/docs/partials/developer-docs.rst new file mode 100644 index 00000000..e6cd4d63 --- /dev/null +++ b/docs/partials/developer-docs.rst @@ -0,0 +1,12 @@ +.. note:: + + This documentation page is aimed at developers who want to customize, + change or extend the code of OpenWISP Notifications in order to modify + its behavior (eg: for personal or commercial purposes or to fix a bug, + implement a new feature or contribute to the project in general). + + If you aren't a developer and you are looking for information on how + to use OpenWISP, please refer to: + + - :doc:`General OpenWISP Quickstart ` + - :doc:`OpenWISP Notifications User Docs ` diff --git a/docs/user/intro.rst b/docs/user/intro.rst new file mode 100644 index 00000000..2ff8ab49 --- /dev/null +++ b/docs/user/intro.rst @@ -0,0 +1,17 @@ +Notifications: Features +======================= + +OpenWISP Notifications offers a robust set of features to keep users +informed about significant events in their network. These features +include: + +- :doc:`sending-notifications` +- :ref:`notifications_web_notifications` +- :ref:`notifications_email_notifications` +- :doc:`notification-types` +- :doc:`User notification preferences ` +- :ref:`Silencing notifications for specific objects temporarily or + permanently ` +- :doc:`Automatic cleanup of old notifications + ` +- :ref:`Configurable host for API endpoints ` diff --git a/docs/user/management-commands.rst b/docs/user/management-commands.rst new file mode 100644 index 00000000..4eb9b64b --- /dev/null +++ b/docs/user/management-commands.rst @@ -0,0 +1,37 @@ +Management Commands +=================== + +.. include:: ../partials/developer-docs.rst + +``populate_notification_preferences`` +------------------------------------- + +This command will populate notification preferences for all users for +organizations they are member of. + +.. note:: + + Before running this command make sure that the celery broker is + running and **reachable** by celery workers. + +Example usage: + +.. code-block:: shell + + # cd tests/ + ./manage.py populate_notification_preferences + +``create_notification`` +----------------------- + +This command will create a dummy notification with ``default`` +notification type for the members of ``default`` organization. This +command is primarily provided for the sole purpose of testing notification +in development only. + +Example usage: + +.. code-block:: shell + + # cd tests/ + ./manage.py create_notification diff --git a/docs/user/notification-cache.rst b/docs/user/notification-cache.rst new file mode 100644 index 00000000..83ffe920 --- /dev/null +++ b/docs/user/notification-cache.rst @@ -0,0 +1,55 @@ +Notification Cache +================== + +In a typical OpenWISP installation, ``actor``, ``action_object`` and +``target`` objects are same for a number of notifications. To optimize +database queries, these objects are cached using `Django's cache framework +`_. The cached values +are updated automatically to reflect actual data from database. You can +control the duration of caching these objects using +:ref:`OPENWISP_NOTIFICATIONS_CACHE_TIMEOUT setting +`. + +Cache Invalidation +------------------ + +The function ``register_notification_cache_update`` can be used to +register a signal of a model which is being used as an ``actor``, +``action_object`` and ``target`` objects. As these values are cached for +the optimization purpose so their cached values are need to be changed +when they are changed. You can register any signal you want which will +delete the cached value. To register a signal you need to include +following code in your ``apps.py``. + +.. code-block:: python + + from django.db.models.signals import post_save + from swapper import load_model + + + def ready(self): + super().ready() + + # Include lines after this inside + # ready function of you app config class + from openwisp_notifications.handlers import ( + register_notification_cache_update, + ) + + model = load_model("app_name", "model_name") + register_notification_cache_update( + model, + post_save, + dispatch_uid="myapp_mymodel_notification_cache_invalidation", + ) + +.. important:: + + You need to import ``register_notification_cache_update`` inside the + ``ready`` function or you can define another function to register + signals which will be called in ``ready`` and then it will be imported + in this function. Also ``dispatch_uid`` is unique identifier of a + signal. You can pass any value you want but it needs to be unique. For + more details read `preventing duplicate signals section of Django + documentation + `_ diff --git a/docs/user/notification-preferences.rst b/docs/user/notification-preferences.rst new file mode 100644 index 00000000..1e2dfa8d --- /dev/null +++ b/docs/user/notification-preferences.rst @@ -0,0 +1,53 @@ +Notification Preferences +======================== + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-settings.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-settings.png + :align: center + +OpenWISP Notifications enables users to customize their notification +preferences by selecting their preferred method of receiving +updates—either through web notifications or email. These settings are +organized by notification type and organization, allowing users to tailor +their notification experience by opting to receive updates only from +specific organizations or notification types. + +Notification settings are automatically generated for all notification +types and organizations for every user. Superusers have the ability to +manage notification settings for all users, including adding or deleting +them. Meanwhile, staff users can modify their preferred notification +delivery methods, choosing between receiving notifications via web, email, +or both. Additionally, users have the option to disable notifications +entirely by turning off both web and email notification settings. + +.. note:: + + If a user has not configured their preferences for email or web + notifications for a specific notification type, the system will + default to using the ``email_notification`` or ``web_notification`` + option defined for that notification type. + +.. _notifications_silencing: + +Silencing Notifications for Specific Objects +-------------------------------------------- + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/silence-notifications.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/silence-notifications.png + :align: center + +OpenWISP Notifications allows users to silence all notifications generated +by specific objects they are not interested in for a desired period of +time or even permanently, while other users will keep receiving +notifications normally. + +Using the widget on an object's admin change form, a user can disable all +notifications generated by that object for a day, week, month or +permanently. + +.. note:: + + This feature requires configuring + :ref:`"OPENWISP_NOTIFICATIONS_IGNORE_ENABLED_ADMIN" + ` to enable the widget in + the admin section of the required models. diff --git a/docs/user/notification-types.rst b/docs/user/notification-types.rst new file mode 100644 index 00000000..91247df1 --- /dev/null +++ b/docs/user/notification-types.rst @@ -0,0 +1,145 @@ +Notification Types +================== + +.. contents:: **Table of contents**: + :depth: 2 + :local: + +OpenWISP Notifications allows defining notification types for recurring +events. Think of a notification type as a template for notifications. + +.. _notifications_generic_message_type: + +``generic_message`` +------------------- + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/1.1/generic_message.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/1.1/generic_message.png + :align: center + +This module includes a notification type called ``generic_message``. + +This notification type is designed to deliver custom messages in the user +interface for infrequent events or errors that occur during background +operations and cannot be communicated easily to the user in other ways. + +These messages may require longer explanations and are therefore displayed +in a dialog overlay, as shown in the screenshot above. This notification +type does not send emails. + +The following code example demonstrates how to send a notification of this +type: + +.. code-block:: python + + from openwisp_notifications.signals import notify + + notify.send( + type="generic_message", + level="error", + message="An unexpected error happened!", + sender=User.objects.first(), + target=User.objects.last(), + description="""Lorem Ipsum is simply dummy text + of the printing and typesetting industry. + + ### Heading 3 + + Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, + when an unknown printer took a galley of type and scrambled it to make a + type specimen book. + + It has survived not only **five centuries**, but also the leap into + electronic typesetting, remaining essentially unchanged. + + It was popularised in the 1960s with the release of Letraset sheets + containing Lorem Ipsum passages, and more recently with desktop publishing + software like Aldus PageMaker including versions of *Lorem Ipsum*.""", + ) + +Properties of Notification Types +-------------------------------- + +The following properties can be configured for each notification type: + +====================== ================================================== +**Property** **Description** +``level`` Sets ``level`` attribute of the notification. +``verb`` Sets ``verb`` attribute of the notification. +``verbose_name`` Sets display name of notification type. +``message`` Sets ``message`` attribute of the notification. +``email_subject`` Sets subject of the email notification. +``message_template`` Path to file having template for message of the + notification. +``email_notification`` Sets preference for email notifications. Defaults + to ``True``. +``web_notification`` Sets preference for web notifications. Defaults to + ``True``. +``actor_link`` Overrides the default URL used for the ``actor`` + object. + + You can pass a static URL or a dotted path to a + callable which returns the object URL. +``action_object_link`` Overrides the default URL used for the ``action`` + object. + + You can pass a static URL or a dotted path to a + callable which returns the object URL. +``target_link`` Overrides the default URL used for the ``target`` + object. + + You can pass a static URL or a dotted path to a + callable which returns the object URL. +====================== ================================================== + +.. note:: + + It is recommended that a notification type configuration for recurring + events contains either the ``message`` or ``message_template`` + properties. If both are present, ``message`` is given preference over + ``message_template``. + + If you don't plan on using ``message`` or ``message_template``, it may + be better to use the existing ``generic_message`` type. However, it's + advised to do so only if the event being notified is infrequent. + +The callable for ``actor_link``, ``action_object_link`` and +``target_link`` should have the following signature: + +.. code-block:: python + + def related_object_link_callable(notification, field, absolute_url=True): + """ + notification: the notification object for which the URL will be created + field: the related object field, any one of "actor", "action_object" or + "target" field of the notification object + absolute_url: boolean to flag if absolute URL should be returned + """ + return "https://custom.domain.com/custom/url/" + +Defining ``message_template`` +----------------------------- + +You can either extend default message template or write your own markdown +formatted message template from scratch. An example to extend default +message template is shown below. + +.. code-block:: django + + # In templates/your_notifications/your_message_template.md + {% extends 'openwisp_notifications/default_message.md' %} + {% block body %} + [{{ notification.target }}]({{ notification.target_link }}) has malfunctioned. + {% endblock body %} + +You can access all attributes of the notification using ``notification`` +variables in your message template as shown above. Additional attributes +``actor_link``, ``action_link`` and ``target_link`` are also available for +providing hyperlinks to respective object. + +.. important:: + + After writing code for registering or unregistering notification + types, it is recommended to run database migrations to create + :doc:`notification settlings ` for these + notification types. diff --git a/docs/user/rest-api.rst b/docs/user/rest-api.rst new file mode 100644 index 00000000..81bae1ad --- /dev/null +++ b/docs/user/rest-api.rst @@ -0,0 +1,189 @@ +REST API +======== + +.. contents:: **Table of Contents**: + :depth: 1 + :local: + +.. _notifications_live_documentation: + +Live Documentation +------------------ + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/api-docs.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/api-docs.png + :align: center + +A general live API documentation (following the OpenAPI specification) is +available at ``/api/v1/docs/``. + +.. _notifications_browsable_web_interface: + +Browsable Web Interface +----------------------- + +.. image:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/api-ui.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/api-ui.png + :align: center + +Additionally, opening any of the endpoints :ref:`listed below +` directly in the browser will show the +`browsable API interface of Django-REST-Framework +`_, which +makes it even easier to find out the details of each endpoint. + +Authentication +-------------- + +See openwisp-users: :ref:`authenticating with the user token +`. + +When browsing the API via the :ref:`notifications_live_documentation` or +the :ref:`notifications_browsable_web_interface`, you can also use the +session authentication by logging in the django admin. + +Pagination +---------- + +The *list* endpoint support the ``page_size`` parameter that allows +paginating the results in conjunction with the ``page`` parameter. + +.. code-block:: text + + GET /api/v1/notifications/notification/?page_size=10 + GET /api/v1/notifications/notification/?page_size=10&page=2 + +.. _notifications_rest_endpoints: + +List of Endpoints +----------------- + +Since the detailed explanation is contained in the +:ref:`notifications_live_documentation` and in the +:ref:`notifications_browsable_web_interface` of each point, here we'll +provide just a list of the available endpoints, for further information +please open the URL of the endpoint in your browser. + +List User's Notifications +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + GET /api/v1/notifications/notification/ + +**Available Filters** + +You can filter the list of notifications based on whether they are read or +unread using the ``unread`` parameter. + +To list read notifications: + +.. code-block:: text + + GET /api/v1/notifications/notification/?unread=false + +To list unread notifications: + +.. code-block:: text + + GET /api/v1/notifications/notification/?unread=true + +Mark All User's Notifications as Read +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + POST /api/v1/notifications/notification/read/ + +Get Notification Details +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + GET /api/v1/notifications/notification/{pk}/ + +Mark a Notification Read +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + PATCH /api/v1/notifications/notification/{pk}/ + +Delete a Notification +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + DELETE /api/v1/notifications/notification/{pk}/ + +List User's Notification Setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + GET /api/v1/notifications/notification/user-setting/ + +**Available Filters** + +You can filter the list of user's notification setting based on their +``organization_id``. + +.. code-block:: text + + GET /api/v1/notifications/notification/user-setting/?organization={organization_id} + +You can filter the list of user's notification setting based on their +``organization_slug``. + +.. code-block:: text + + GET /api/v1/notifications/notification/user-setting/?organization_slug={organization_slug} + +You can filter the list of user's notification setting based on their +``type``. + +.. code-block:: text + + GET /api/v1/notifications/notification/user-setting/?type={type} + +Get Notification Setting Details +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + GET /api/v1/notifications/notification/user-setting/{pk}/ + +Update Notification Setting Details +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + PATCH /api/v1/notifications/notification/user-setting/{pk}/ + +List User's Object Notification Setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + GET /api/v1/notifications/notification/ignore/ + +Get Object Notification Setting Details +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + GET /api/v1/notifications/notification/ignore/{app_label}/{model_name}/{object_id}/ + +Create Object Notification Setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + PUT /api/v1/notifications/notification/ignore/{app_label}/{model_name}/{object_id}/ + +Delete Object Notification Setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: text + + DELETE /api/v1/notifications/notification/ignore/{app_label}/{model_name}/{object_id}/ diff --git a/docs/user/scheduled-deletion-of-notifications.rst b/docs/user/scheduled-deletion-of-notifications.rst new file mode 100644 index 00000000..9fe76139 --- /dev/null +++ b/docs/user/scheduled-deletion-of-notifications.rst @@ -0,0 +1,39 @@ +Scheduled Deletion of Notifications +=================================== + +.. note:: + + If you have deployed OpenWISP using :doc:`ansible-openwisp2 + ` or :doc:`docker-openwisp `, then this + feature has been already configured for you. Refer to the + documentation of your deployment method to know the default value. + This section is only for reference for users who wish to customize + OpenWISP, or who have deployed OpenWISP in a different way. + +OpenWISP Notifications provides a celery task to automatically delete +notifications older than a pre-configured number of days. In order to run +this task periodically, you will need to configure +``CELERY_BEAT_SCHEDULE``. + +The celery task takes only one argument, i.e. number of days. You can +provide any number of days in `args` key while configuring +``CELERY_BEAT_SCHEDULE`` setting. + +E.g., if you want notifications older than 10 days to get deleted +automatically, then configure ``CELERY_BEAT_SCHEDULE`` as follows: + +.. code-block:: python + + CELERY_BEAT_SCHEDULE = { + "delete_old_notifications": { + "task": "openwisp_notifications.tasks.delete_old_notifications", + "schedule": timedelta(days=1), + "args": ( + 10, + ), # Here we have defined 10 instead of 90 as shown in setup instructions + }, + } + +Please refer to `"Periodic Tasks" section of Celery's documentation +`_ +to learn more. diff --git a/docs/user/sending-notifications.rst b/docs/user/sending-notifications.rst new file mode 100644 index 00000000..a512748c --- /dev/null +++ b/docs/user/sending-notifications.rst @@ -0,0 +1,133 @@ +Sending Notifications +===================== + +.. contents:: **Table of contents**: + :depth: 2 + :local: + +The ``notify`` signal +--------------------- + +Notifications can be created using the ``notify`` signal. Here's an +example which uses the :ref:`generic_message +` notification type to alert users of +an account being deactivated: + +.. code-block:: python + + from django.contrib.auth import get_user_model + from swapper import load_model + + from openwisp_notifications.signals import notify + + User = get_user_model() + admin = User.objects.get(username="admin") + deactivated_user = User.objects.get(username="johndoe", is_active=False) + + notify.send( + sender=admin, + type="generic_message", + level="info", + target=deactivated_user, + message="{notification.actor} has deactivated {notification.target}", + ) + +The above snippet will send notifications to all superusers and +organization administrators of the target object's organization who have +opted-in to receive notifications. If the target object is omitted or does +not have an organization, it will only send notifications to superusers. + +You can override the recipients of the notification by passing the +``recipient`` keyword argument. The ``recipient`` argument can be a: + +- ``Group`` object +- A list or queryset of ``User`` objects +- A single ``User`` object + +However, these users will only be notified if they have opted-in to +receive notifications. + +The complete syntax for ``notify`` is: + +.. code-block:: python + + notify.send( + actor, + recipient, + verb, + action_object, + target, + level, + description, + **kwargs, + ) + +Since ``openwisp-notifications`` uses ``django-notifications`` under the +hood, usage of the ``notify signal`` has been kept unaffected to maintain +consistency with ``django-notifications``. You can learn more about +accepted parameters from `django-notifications documentation +`_. + +The ``notify`` signal supports the following additional parameters: + +================= ====================================================== +**Parameter** **Description** +``type`` Set values of other parameters based on registered + :doc:`notification types <./notification-types>` + + Defaults to ``None`` meaning you need to provide other + arguments. +``email_subject`` Sets subject of email notification to be sent. + + Defaults to the notification message. +``url`` Adds a URL in the email text, eg: + + ``For more information see .`` + + Defaults to ``None``, meaning the above message would + not be added to the email text. +================= ====================================================== + +Passing Extra Data to Notifications +----------------------------------- + +If needed, additional data, not known beforehand, can be included in the +notification message. + +A perfect example for this case is an error notification, the error +message will vary depending on what has happened, so we cannot know until +the notification is generated. + +Here's how to do it: + +.. code-block:: python + + from openwisp_notifications.types import register_notification_type + + register_notification_type( + "error_type", + { + "verbose_name": "Error", + "level": "error", + "verb": "error", + "message": "Error: {error}", + "email_subject": "Error subject: {error}", + }, + ) + +Then in the application code: + +.. code-block:: python + + from openwisp_notifications.signals import notify + + try: + operation_which_can_fail() + except Exception as error: + notify.send(type="error_type", sender=sender, error=str(error)) + +Since the ``error_type`` notification type defined the notification +message, you don't need to pass the ``message`` argument in the notify +signal. The message defined in the notification type will be used by the +notification. The ``error`` argument is used to set the value of the +``{error}`` placeholder in the notification message. diff --git a/docs/user/settings.rst b/docs/user/settings.rst new file mode 100644 index 00000000..451704c5 --- /dev/null +++ b/docs/user/settings.rst @@ -0,0 +1,158 @@ +Settings +======== + +.. include:: /partials/settings-note.rst + +.. _openwisp_notifications_host: + +``OPENWISP_NOTIFICATIONS_HOST`` +------------------------------- + +======= ====================================== +type ``str`` +default Any domain defined in ``ALLOWED_HOST`` +======= ====================================== + +This setting defines the domain at which API and Web Socket communicate +for working of notification widget. + +.. note:: + + You don't need to configure this setting if you don't host your API + endpoints on a different sub-domain. + +If your root domain is ``example.com`` and API and Web Socket are hosted +at ``api.example.com``, then configure setting as follows: + +.. code-block:: python + + OPENWISP_NOTIFICATIONS_HOST = "https://api.example.com" + +This feature requires you to allow `CORS +`_ on your server. +We use ``django-cors-headers`` module to easily setup CORS headers. Please +refer `django-core-headers' setup documentation +`_. + +Configure ``django-cors-headers`` settings as follows: + +.. code-block:: python + + CORS_ALLOW_CREDENTIALS = True + CORS_ORIGIN_WHITELIST = ["https://www.example.com"] + +Configure Django's settings as follows: + +.. code-block:: python + + SESSION_COOKIE_DOMAIN = "example.com" + CSRF_COOKIE_DOMAIN = "example.com" + +Please refer to `Django's settings documentation +`_ for more +information on ``SESSION_COOKIE_DOMAIN`` and ``CSRF_COOKIE_DOMAIN`` +settings. + +``OPENWISP_NOTIFICATIONS_SOUND`` +-------------------------------- + +======= =================================================================================================================================================== +type ``str`` +default `notification_bell.mp3 + `_ +======= =================================================================================================================================================== + +This setting defines notification sound to be played when notification is +received in real-time on admin site. + +Provide a relative path (hosted on your webserver) to audio file as show +below. + +.. code-block:: python + + OPENWISP_NOTIFICATIONS_SOUND = "your-appname/audio/notification.mp3" + +.. _openwisp_notifications_cache_timeout: + +``OPENWISP_NOTIFICATIONS_CACHE_TIMEOUT`` +---------------------------------------- + +======= ================================= +type ``int`` +default ``172800`` `(2 days, in seconds)` +======= ================================= + +It sets the number of seconds the notification contents should be stored +in the cache. If you want cached notification content to never expire, +then set it to ``None``. Set it to ``0`` if you don't want to store +notification contents in cache at all. + +.. _openwisp_notifications_ignore_enabled_admin: + +``OPENWISP_NOTIFICATIONS_IGNORE_ENABLED_ADMIN`` +----------------------------------------------- + +======= ======== +type ``list`` +default [] +======= ======== + +This setting enables the widget which allows users to :ref:`silence +notifications for specific objects temporarily or permanently. +` in the change page of the specified +``ModelAdmin`` classes. + +E.g., if you want to enable the widget for objects of +``openwisp_users.models.User`` model, then configure the setting as +following: + +.. code-block:: python + + OPENWISP_NOTIFICATIONS_IGNORE_ENABLED_ADMIN = [ + "openwisp_users.admin.UserAdmin" + ] + +``OPENWISP_NOTIFICATIONS_POPULATE_PREFERENCES_ON_MIGRATE`` +---------------------------------------------------------- + +======= ======== +type ``bool`` +default ``True`` +======= ======== + +This setting allows to disable creating :doc:`notification preferences +` on running migrations. + +``OPENWISP_NOTIFICATIONS_NOTIFICATION_STORM_PREVENTION`` +-------------------------------------------------------- + +When the system starts creating a lot of notifications because of a +general network outage (e.g.: a power outage, a global misconfiguration), +the notification storm prevention mechanism avoids the constant displaying +of new notification alerts as well as their sound, only the notification +counter will continue updating periodically, although it won't emit any +sound or create any other visual element until the notification storm is +over. + +This setting allows tweaking how this mechanism works. + +The default configuration is as follows: + +.. code-block:: python + + OPENWISP_NOTIFICATIONS_NOTIFICATION_STORM_PREVENTION = { + # Time period for tracking burst of notifications (in seconds) + "short_term_time_period": 10, + # Number of notifications considered as a notification burst + "short_term_notification_count": 6, + # Time period for tracking notifications in long time interval (in seconds) + "long_term_time_period": 180, + # Number of notifications in long time interval to be considered as a notification storm + "long_term_notification_count": 30, + # Initial time for which notification updates should be skipped (in seconds) + "initial_backoff": 1, + # Time by which skipping of notification updates should be increased (in seconds) + "backoff_increment": 1, + # Maximum interval after which the notification widget should get updated (in seconds) + "max_allowed_backoff": 15, + } diff --git a/docs/user/web-email-notifications.rst b/docs/user/web-email-notifications.rst new file mode 100644 index 00000000..afe4da6c --- /dev/null +++ b/docs/user/web-email-notifications.rst @@ -0,0 +1,56 @@ +Web & Email Notifications +========================= + +.. contents:: **Table of Contents**: + :depth: 2 + :local: + +.. _notifications_web_notifications: + +Web Notifications +----------------- + +OpenWISP Notifications sends web notifications to recipients through +Django's admin site. The following components facilitate browsing web +notifications: + +Notification Widget +~~~~~~~~~~~~~~~~~~~ + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-widget.gif + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-widget.gif + :align: center + +A JavaScript widget has been added to make consuming notifications easy +for users. The notification widget provides the following features: + +- A minimalistic UI to help users complete tasks quickly. +- Dynamically loads notifications with infinite scrolling to prevent + unnecessary network requests. +- Option to filter unread notifications. +- Option to mark all notifications as read with a single click. + +Notification Toasts +~~~~~~~~~~~~~~~~~~~ + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-toast.gif + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/notification-toast.gif + :align: center + +Notification toast delivers notifications in real-time, allowing users to +read notifications without opening the notification widget. A notification +bell sound is played each time a notification is displayed through the +notification toast. + +.. _notifications_email_notifications: + +Email Notifications +------------------- + +.. figure:: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/email-template.png + :target: https://raw.githubusercontent.com/openwisp/openwisp-notifications/docs/docs/images/email-template.png + :align: center + +Along with web notifications OpenWISP Notifications also sends email +notifications leveraging the :ref:`send_email feature of OpenWISP Utils +`. diff --git a/pyproject.toml b/pyproject.toml index 88a860d0..a1b2561e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,10 @@ omit = [ ] [tool.docstrfmt] -extend_exclude = ["**/*.py", "README.rst"] +extend_exclude = ["**/*.py"] [tool.isort] -known_third_party = ["django", "django_x509"] +known_third_party = ["django", "django_x509", "notifications"] known_first_party = ["openwisp_users", "openwisp_utils", "openwisp_notifications"] default_section = "THIRDPARTY" line_length = 88