diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fcf028c9..d31d835e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,12 +45,15 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + **/requirements*.txt - name: Install Dependencies id: deps run: | - sudo apt update - sudo apt-get -qq -y install sqlite3 gdal-bin + sudo apt -qq update + sudo apt -qq -y install sqlite3 gdal-bin pip install -U pip wheel setuptools pip install -U -r requirements-test.txt pip install -U -e . diff --git a/README.rst b/README.rst index 70d7b626..d61cc903 100644 --- a/README.rst +++ b/README.rst @@ -1,39 +1,38 @@ -====================== OpenWISP Notifications ====================== .. image:: https://github.com/openwisp/openwisp-notifications/workflows/OpenWISP%20CI%20Build/badge.svg?branch=master - :target: https://github.com/openwisp/openwisp-notifications/actions?query=workflow%3A%22OpenWISP+CI+Build%22 - :alt: CI build status + :target: https://github.com/openwisp/openwisp-notifications/actions?query=workflow%3A%22OpenWISP+CI+Build%22 + :alt: CI build status .. image:: https://coveralls.io/repos/github/openwisp/openwisp-notifications/badge.svg?branch=master - :target: https://coveralls.io/github/openwisp/openwisp-notifications?branch=master - :alt: Test Coverage + :target: https://coveralls.io/github/openwisp/openwisp-notifications?branch=master + :alt: Test Coverage .. image:: https://img.shields.io/librariesio/github/openwisp/openwisp-notifications - :target: https://libraries.io/github/openwisp/openwisp-notifications#repository_dependencies - :alt: Dependency monitoring + :target: https://libraries.io/github/openwisp/openwisp-notifications#repository_dependencies + :alt: Dependency monitoring .. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg - :target: https://gitter.im/openwisp/general - :alt: chat + :target: https://gitter.im/openwisp/general + :alt: chat .. image:: https://badge.fury.io/py/openwisp-notifications.svg - :target: http://badge.fury.io/py/openwisp-notifications - :alt: Pypi Version + :target: http://badge.fury.io/py/openwisp-notifications + :alt: Pypi Version .. image:: https://pepy.tech/badge/openwisp-notifications - :target: https://pepy.tech/project/openwisp-notifications - :alt: downloads + :target: https://pepy.tech/project/openwisp-notifications + :alt: downloads .. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://pypi.org/project/black/ - :alt: code style: black + :target: https://pypi.org/project/black/ + :alt: code style: black ------------- +---- .. figure:: https://github.com/openwisp/openwisp-notifications/raw/docs/docs/images/notification-demo.gif - :align: center + :align: center **OpenWISP Notifications** provides email and web notifications for `OpenWISP `_. @@ -41,16 +40,16 @@ OpenWISP Notifications Its main goal is to allow the other OpenWISP modules to notify users about meaningful events that happen in their network. -**For a more complete overview of the OpenWISP modules and architecture**, -see the -`OpenWISP Architecture Overview -`_. +For a complete overview of features, refer to the `Notifications: Features +`_ section of +the OpenWISP documentation. ------------- +Documentation +------------- -.. contents:: **Table of Contents**: - :backlinks: none - :depth: 3 +- `Usage documentation `_ +- `Developer documentation + `_ ------------ @@ -1566,16 +1565,21 @@ Create a consumer file as done in `sample_notifications/consumers.py `_. +======= +---- +>>>>>>> master Contributing ------------ -Please read the `OpenWISP contributing guidelines `_. +Please read the `OpenWISP contributing guidelines +`_. License ------- -See `LICENSE `_. +See `LICENSE +`_. Support ------- @@ -1588,15 +1592,18 @@ Attributions Icons ~~~~~ -`Icons `_ +`Icons +`_ used are taken from `Font Awesome `_ project. -LICENSE: `https://fontawesome.com/license `_ +LICENSE: https://fontawesome.com/license Sound ~~~~~ -`Notification sound `_ +`Notification sound +`_ is taken from `Notification Sounds `_. -LICENSE: `Creative Commons Attribution license `_ +LICENSE: `Creative Commons Attribution license +`_ diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst new file mode 100644 index 00000000..a91c54d9 --- /dev/null +++ b/docs/developer/extending.rst @@ -0,0 +1,362 @@ +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, you will need to register them 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 preceding 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..627887e2 --- /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 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..b391cf46 --- /dev/null +++ b/docs/partials/developer-docs.rst @@ -0,0 +1,9 @@ +.. note:: + + This page is for developers who want to customize or extend OpenWISP + Notifications, whether for bug fixes, new features, or contributions. + + For user guides and general information, please see: + + - :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..b5e530be --- /dev/null +++ b/docs/user/scheduled-deletion-of-notifications.rst @@ -0,0 +1,43 @@ +Scheduled Deletion of Notifications +=================================== + +.. important:: + + 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 preconfigured number of days. In order to run +this task periodically, you will need to configure +``CELERY_BEAT_SCHEDULE`` in the Django project settings. + +.. include:: /partials/settings-note.rst + +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.update( + { + "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..2abe62cc --- /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, e.g.: + + ``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..1ad84bb1 --- /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 web server) 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..dd57a332 --- /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: + +- User Interface 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/error.txt b/error.txt new file mode 100644 index 00000000..e2f04710 --- /dev/null +++ b/error.txt @@ -0,0 +1,16 @@ +====================================================================== +ERROR: test_notification_setting_inline_add_permission (openwisp_notifications.tests.test_admin.TestAdmin) [Test for superuser] +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/dhanus/Projects/OSS/OpenWISP/openwisp-notifications/openwisp_notifications/tests/test_admin.py", line 172, in test_notification_setting_inline_add_permission + self.assertTrue(self.ns_inline.has_add_permission(su_request)) +AttributeError: 'TestAdmin' object has no attribute 'ns_inline' + +====================================================================== +ERROR: test_notification_setting_inline_add_permission (openwisp_notifications.tests.test_admin.TestAdmin) [Test for non-superuser] +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/dhanus/Projects/OSS/OpenWISP/openwisp-notifications/openwisp_notifications/tests/test_admin.py", line 176, in test_notification_setting_inline_add_permission + self.ns_inline.has_add_permission(op_request), +AttributeError: 'TestAdmin' object has no attribute 'ns_inline' + diff --git a/index.html b/index.html new file mode 100644 index 00000000..8526a2c8 --- /dev/null +++ b/index.html @@ -0,0 +1,57 @@ + + + + + + Question Mark Icon with Tooltip + + + + + + diff --git a/openwisp_notifications/base/forms.py b/openwisp_notifications/base/forms.py index 4b1b91f5..b2bc6add 100644 --- a/openwisp_notifications/base/forms.py +++ b/openwisp_notifications/base/forms.py @@ -9,9 +9,11 @@ def __init__(self, *args, **kwargs): if instance: kwargs['initial'] = { 'web': instance.web_notification, - 'email': instance.email_notification - if instance.web_notification - else instance.web_notification, + 'email': ( + instance.email_notification + if instance.web_notification + else instance.web_notification + ), } super().__init__(*args, **kwargs) try: diff --git a/openwisp_notifications/static/openwisp-notifications/images/icons/icon-gear.svg b/openwisp_notifications/static/openwisp-notifications/images/icons/icon-gear.svg new file mode 100644 index 00000000..815961c0 --- /dev/null +++ b/openwisp_notifications/static/openwisp-notifications/images/icons/icon-gear.svg @@ -0,0 +1,3 @@ + + + diff --git a/openwisp_notifications/tests/test_api.py b/openwisp_notifications/tests/test_api.py index 5d5481aa..56f53abc 100644 --- a/openwisp_notifications/tests/test_api.py +++ b/openwisp_notifications/tests/test_api.py @@ -27,8 +27,6 @@ Organization = swapper_load_model('openwisp_users', 'Organization') OrganizationUser = swapper_load_model('openwisp_users', 'OrganizationUser') -NOT_FOUND_ERROR = ErrorDetail(string='Not found.', code='not_found') - class TestNotificationApi( TransactionTestCase, TestOrganizationMixin, AuthenticationMixin @@ -189,10 +187,6 @@ def test_retreive_notification_api(self): url = self._get_path('notification_detail', uuid.uuid4()) response = self.client.get(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual( - response.data, - {'detail': NOT_FOUND_ERROR}, - ) with self.subTest('Test retrieving details for existing notification'): url = self._get_path('notification_detail', n.pk) @@ -212,10 +206,6 @@ def test_read_single_notification_api(self): url = self._get_path('notification_detail', uuid.uuid4()) response = self.client.patch(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual( - response.data, - {'detail': NOT_FOUND_ERROR}, - ) with self.subTest('Test for existing notification'): self.assertTrue(n.unread) @@ -234,10 +224,6 @@ def test_notification_delete_api(self): url = self._get_path('notification_detail', uuid.uuid4()) response = self.client.delete(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual( - response.data, - {'detail': NOT_FOUND_ERROR}, - ) with self.subTest('Test for valid notification'): url = self._get_path('notification_detail', n.pk) @@ -404,13 +390,11 @@ def test_notification_recipients(self): url = self._get_path('notification_detail', n.pk) response = self.client.get(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) with self.subTest('Test marking a notification as read'): url = self._get_path('notification_detail', n.pk) response = self.client.patch(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) # Check Karen's notification is still unread n.refresh_from_db() self.assertTrue(n.unread) @@ -419,7 +403,6 @@ def test_notification_recipients(self): url = self._get_path('notification_detail', n.pk) response = self.client.delete(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) # Check Karen's notification is not deleted self.assertEqual(Notification.objects.count(), 1) @@ -468,7 +451,6 @@ def test_malformed_notifications(self): url = self._get_path('notification_detail', n.pk) response = self.client.get(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) @capture_any_output() @mock_notification_types @@ -514,7 +496,6 @@ def test_obsolete_notifications_busy_worker(self, mocked_task): url = self._get_path('notification_read_redirect', notification.pk) response = self.client.get(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) def test_notification_setting_list_api(self): self._create_org_user(is_admin=True) @@ -618,10 +599,6 @@ def test_retreive_notification_setting_api(self): url = self._get_path('notification_setting', uuid.uuid4()) response = self.client.get(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual( - response.data, - {'detail': NOT_FOUND_ERROR}, - ) with self.subTest('Test retrieving details for existing notification setting'): url = self._get_path( @@ -644,10 +621,6 @@ def test_update_notification_setting_api(self): url = self._get_path('notification_setting', uuid.uuid4()) response = self.client.put(url, data=update_data) self.assertEqual(response.status_code, 404) - self.assertDictEqual( - response.data, - {'detail': NOT_FOUND_ERROR}, - ) with self.subTest('Test retrieving details for existing notification setting'): url = self._get_path( @@ -677,7 +650,6 @@ def _unread_notification(notification): url = self._get_path('notification_read_redirect', uuid.uuid4()) response = self.client.get(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) with self.subTest('Test existent notification'): url = self._get_path('notification_read_redirect', notification.pk) @@ -767,10 +739,6 @@ def test_delete_ignore_obj_notification_api(self, mocked_task): ) response = self.client.delete(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual( - response.data, - {'detail': NOT_FOUND_ERROR}, - ) with self.subTest('Test for existing object notification'): url = self._get_path( @@ -799,10 +767,6 @@ def test_retrieve_ignore_obj_notification_api(self, mocked_task): ) response = self.client.delete(url) self.assertEqual(response.status_code, 404) - self.assertDictEqual( - response.data, - {'detail': NOT_FOUND_ERROR}, - ) with self.subTest('Test for existing object notification'): url = self._get_path( diff --git a/pyproject.toml b/pyproject.toml index 88a860d0..ecf3108a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,13 @@ [tool.coverage.run] source = ["openwisp_notifications"] parallel = true -concurrency = ["multiprocessing"] +# To ensure correct coverage, we need to include both +# "multiprocessing" and "thread" in the concurrency list. +# This is because Django test suite incorrectly reports coverage +# when "multiprocessing" is omitted and the "--parallel" flag +# is used. Similarly, coverage for websocket consumers is +# incorrect when "thread" is omitted and pytest is used. +concurrency = ["multiprocessing", "thread"] omit = [ "openwisp_notifications/__init__.py", "*/tests/*", @@ -9,10 +15,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