diff --git a/.coveragerc b/.coveragerc index ac42c385..f58f2204 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,8 @@ [run] +source = openwisp_utils +parallel = true +concurrency = multiprocessing omit = /*/test* - /tests /*/__init__.py - /setup.py /*/migrations/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 887c7748..ac534f2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,9 @@ on: - dev jobs: - build: name: Python==${{ matrix.python-version }} | ${{ matrix.django-version }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -30,12 +29,12 @@ jobs: - django~=4.2.0 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -53,27 +52,27 @@ jobs: - name: Tests if: ${{ !cancelled() && steps.deps.conclusion == 'success' }} - run: coverage run --source=openwisp_utils runtests.py + run: | + coverage run runtests.py --parallel + coverage combine + coverage xml env: SELENIUM_HEADLESS: 1 - name: Upload Coverage if: ${{ success() }} - run: coveralls --service=github - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: python-${{ matrix.python-version }}-${{ matrix.django-version }} - COVERALLS_PARALLEL: true + uses: coverallsapp/github-action@v2 + with: + parallel: true + format: cobertura + flag-name: python-${{ matrix.env.env }} + github-token: ${{ secrets.GITHUB_TOKEN }} coveralls: - name: Finish Coveralls needs: build runs-on: ubuntu-latest - container: python:3-slim steps: - - name: Finished - run: | - pip3 install --upgrade coveralls - coveralls --finish - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true diff --git a/CHANGES.rst b/CHANGES.rst index 4f2da825..ac636074 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,9 +12,9 @@ Version 1.0.4 [2022-10-07] Bugfixes ~~~~~~~~ -- Fixed **importlib-metadata** dependency, pinned it to ``<5.0``. - The newer versions of **importlib-metadata** breaks openwisp-utils - on **Python 3.7**. +- Fixed **importlib-metadata** dependency, pinned it to ``<5.0``. The + newer versions of **importlib-metadata** breaks openwisp-utils on + **Python 3.7**. Version 1.0.3 [2022-08-03] -------------------------- @@ -22,8 +22,8 @@ Version 1.0.3 [2022-08-03] Bugfixes ~~~~~~~~ -- Fixed **django-fitler** dependency, pinned it to ``~=21.1``. - Earlier, it was installing the latest version of django-filter. +- Fixed **django-fitler** dependency, pinned it to ``~=21.1``. Earlier, it + was installing the latest version of django-filter. Version 1.0.2 [2022-07-01] -------------------------- @@ -31,19 +31,17 @@ Version 1.0.2 [2022-07-01] Bugfixes ~~~~~~~~ -- Fixed empty charts showing annotations from - the previous chart -- Fixed dashboard template ``extra_config`` getting - over-written when multiple dashboard templates - are used +- Fixed empty charts showing annotations from the previous chart +- Fixed dashboard template ``extra_config`` getting over-written when + multiple dashboard templates are used - Fixed `empty dashboard charts not displaying total as "0" `_ Version 1.0.1 [2022-04-07] -------------------------- -- Fixed ``ImportError`` in click dependency of black - (updated black dependency to ``black~=22.3.0``) +- Fixed ``ImportError`` in click dependency of black (updated black + dependency to ``black~=22.3.0``) - Fixed target link of pie charts which use group by queries Version 1.0.0 [2022-02-18] @@ -52,45 +50,60 @@ Version 1.0.0 [2022-02-18] Features ~~~~~~~~ -- Added `customizable navigation menu `_ -- Added `horizontal filters `_ -- Added `customizable admin dashboard `_ -- Added `send_email function `_ -- Added `CompressStaticFilesStorage `_ - - a static storage backend for Django that also compresses static files -- Added `AssertNumQueriesSubTestMixin `_ -- Added `HelpTextStackedInline admin class `_ -- Added `OpenwispCeleryTask `_ - a custom celery task class -- Added support for linting CSS and JS in `openwisp-qa-check `_ -- Added support for formatting CSS and JS in `openwisp-qa-format `_ -- Added `git pre-push hook `_ +- Added `customizable navigation menu + `_ +- Added `horizontal filters + `_ +- Added `customizable admin dashboard + `_ +- Added `send_email function + `_ +- Added `CompressStaticFilesStorage + `_ + - a static storage backend for Django that also compresses static files +- Added `AssertNumQueriesSubTestMixin + `_ +- Added `HelpTextStackedInline admin class + `_ +- Added `OpenwispCeleryTask + `_ + - a custom celery task class +- Added support for linting CSS and JS in `openwisp-qa-check + `_ +- Added support for formatting CSS and JS in `openwisp-qa-format + `_ +- Added `git pre-push hook + `_ Changes ~~~~~~~ -- `Updated OpenWISP's admin theme `__ +- `Updated OpenWISP's admin theme + `__ **Dependencies**: - Bumped ``django-model-utils~=4.2.0`` - Bumped ``black<=21.10b0`` - Bumped ``djangorestframework~=3.13.0`` -- Added ``swapper~=1.3.0``, ``django-compress-staticfiles~=1.0.1b`` and ``celery~=5.2.3`` +- Added ``swapper~=1.3.0``, ``django-compress-staticfiles~=1.0.1b`` and + ``celery~=5.2.3`` - Added support for Django ``3.2.x`` and ``4.0.x`` - Added support for Python ``3.9`` Bugfixes ~~~~~~~~ -- Fixed `checkcommit` failing for `trailing period (.) after closing keyword `_ +- Fixed `checkcommit` failing for `trailing period (.) after closing + keyword `_ Version 0.7.5 [2021-06-01] -------------------------- - [fix] Added workaround for minification of browsable API view. Django-pipeline strips spaces from pre-formatted text on minifying HTML - which destroys the representation of data on browsable API views. - Added a workaround to restore presentation to original form using CSS. + which destroys the representation of data on browsable API views. Added + a workaround to restore presentation to original form using CSS. Version 0.7.4 [2021-04-08] -------------------------- @@ -111,9 +124,11 @@ Version 0.7.2 [2020-12-11] Version 0.7.1 [2020-11-18] -------------------------- -- [fix] Fixed bug in``openwisp_utils.admin.UUIDAdmin`` which caused the removal of all - the ``readonly_fields`` from the admin add page, now only the ``uuid`` field is removed -- [change] Changed commit check to allow commit messages from `Dependabot `_ +- [fix] Fixed bug in``openwisp_utils.admin.UUIDAdmin`` which caused the + removal of all the ``readonly_fields`` from the admin add page, now only + the ``uuid`` field is removed +- [change] Changed commit check to allow commit messages from `Dependabot + `_ Version 0.7.0 [2020-11-13] -------------------------- @@ -121,15 +136,20 @@ Version 0.7.0 [2020-11-13] Features ~~~~~~~~ -- [qa] Added a `ReStructuredText syntax check (checkrst) `_ - to ``openwisp-qa-check``, which allows to ensure ``README.rst`` and other top level rst files - do not contain syntax errors -- [utils] Added `register_menu_items `_ +- [qa] Added a `ReStructuredText syntax check (checkrst) + `_ to + ``openwisp-qa-check``, which allows to ensure ``README.rst`` and other + top level rst files do not contain syntax errors +- [utils] Added `register_menu_items + `_ to easily register menu items -- [tests] Added test utilities to capture output (eg: to make assertions on it): - `capture_stdout `_, - `capture_stderr `_, - `capture_any_output `_ +- [tests] Added test utilities to capture output (eg: to make assertions + on it): `capture_stdout + `_, + `capture_stderr + `_, + `capture_any_output + `_ Changes ~~~~~~~ @@ -140,7 +160,8 @@ Bugfixes ~~~~~~~~ - [admin] Hide menu options for unauthenticated users -- [admin] Fixed menu buttons being clicked on some sections of page when not visible +- [admin] Fixed menu buttons being clicked on some sections of page when + not visible Version 0.6.3 [2020-09-02] -------------------------- @@ -151,7 +172,8 @@ Version 0.6.2 [2020-08-29] -------------------------- - [fix] Fixed commit message check when close/fix keyword is missing -- [change] Changed QA commit check prefix hint to mention conventional commit prefixes +- [change] Changed QA commit check prefix hint to mention conventional + commit prefixes Version 0.6.1 [2020-08-17] -------------------------- @@ -169,19 +191,20 @@ Features - [admin] ``TestReadOnlyAdmin``: added support for exclude attribute Changes -~~~~~~~~ +~~~~~~~ - [change] Changed QA checks to use isort~=5.0 instead of isort<=4.3; **this will cause changes to the way the code is formatted** -- Always execute ``commitcheck`` when run locally - (on travis it will be run only in pull requests) +- Always execute ``commitcheck`` when run locally (on travis it will be + run only in pull requests) Bugfixes ~~~~~~~~ - [admin] Fixed a bug which caused some menu items to be shown also if the user did not have permission to view or edit them -- [qa] Fixed a regression which caused ``commitcheck`` to not be run on travis pull requests +- [qa] Fixed a regression which caused ``commitcheck`` to not be run on + travis pull requests - [tests] Fixed ``SITE_ID`` in test project settings Version 0.5.1 [2020-06-29] @@ -209,7 +232,8 @@ Version 0.5.0 [2020-06-02] - [change] Renamed test_api to api for consistency - [change] Rename openwisp-utils-qa-checks to openwisp-qa-check - [change][api] Renamed /api/v1/swagger/ to /api/v1/docs/ -- [improvement] Moved to importlib for Dependency loader & staticfiles for importing files +- [improvement] Moved to importlib for Dependency loader & staticfiles for + importing files - [improvement] Added "Related to #" for commit-check - [enchancement] Added strict mode to run-qa-checks @@ -218,9 +242,11 @@ Version 0.4.5 [2020-04-07] - [admin-theme] Minor CSS improvements for login-form - [tests] Added ``catch_signal`` test utility -- [qa] Added ``coveralls`` (and hence coverage) to ``extra_requires['qa']`` +- [qa] Added ``coveralls`` (and hence coverage) to + ``extra_requires['qa']`` - [qa] Added merge cases to cases to skip in commit check -- [qa] Added ``--force-checkcommit`` argument to force message commit check +- [qa] Added ``--force-checkcommit`` argument to force message commit + check Version 0.4.4 [2020-02-28] -------------------------- @@ -232,7 +258,8 @@ Version 0.4.4 [2020-02-28] Version 0.4.3 [2020-02-26] -------------------------- -- [utils] Added optional ``receive_url_baseurl`` and ``receive_url_urlconf`` to ``ReceiveUrlAdmin`` +- [utils] Added optional ``receive_url_baseurl`` and + ``receive_url_urlconf`` to ``ReceiveUrlAdmin`` - [menu] Fixed JS error in popup pages (which have no header) - [utils] ``KeyField`` now allows overrding ``default`` and ``validators`` @@ -245,10 +272,10 @@ Version 0.4.2 [2020-01-25] Version 0.4.1 [2020-01-20] -------------------------- -- Added utilities commonly used in other OpenWISP modules: - ``UUIDAdmin``, ``KeyField``, ``ReceiveUrlAdmin``, ``get_random_key`` -- Fixed a minor issue regarding a new line ``\n`` not being formatted properly - in ``openwisp-utils-qa-check`` +- Added utilities commonly used in other OpenWISP modules: ``UUIDAdmin``, + ``KeyField``, ``ReceiveUrlAdmin``, ``get_random_key`` +- Fixed a minor issue regarding a new line ``\n`` not being formatted + properly in ``openwisp-utils-qa-check`` Version 0.4.0 [2020-01-13] -------------------------- @@ -259,16 +286,20 @@ Version 0.4.0 [2020-01-13] Version 0.3.2 [2020-01-09] -------------------------- -- [change] Simplified implementation and usage of ``OPENWISP_ADMIN_SITE_CLASS`` +- [change] Simplified implementation and usage of + ``OPENWISP_ADMIN_SITE_CLASS`` Version 0.3.1 [2020-01-07] -------------------------- -- [feature] Added configurable ``AdminSite`` class and ``OPENWISP_ADMIN_SITE_CLASS`` +- [feature] Added configurable ``AdminSite`` class and + ``OPENWISP_ADMIN_SITE_CLASS`` - [theme] Adapted theme to django 2.2 - [qa] openwisp-utils-qa-checks now runs all checks before failing -- [qa] Added support for multiple migration name check in openwisp-utils-qa-checks -- [qa] Added pending migrations check (``runcheckpendingmigrations``) to openwisp-utils-qa-checks +- [qa] Added support for multiple migration name check in + openwisp-utils-qa-checks +- [qa] Added pending migrations check (``runcheckpendingmigrations``) to + openwisp-utils-qa-checks Version 0.3.0 [2019-12-10] -------------------------- @@ -276,28 +307,29 @@ Version 0.3.0 [2019-12-10] - Added ``ReadOnlyAdmin`` - Added ``AlwaysHasChangedMixin`` - Added ``UUIDModel`` -- Moved multitenancy features to - `openwisp-users `_ -- [qa] Added ``checkendline``, ``checkmigrations``, ``checkcommit``, - later integrated in ``openwisp-utils-qa-checks`` (corrected) +- Moved multitenancy features to `openwisp-users + `_ +- [qa] Added ``checkendline``, ``checkmigrations``, ``checkcommit``, later + integrated in ``openwisp-utils-qa-checks`` (corrected) - Added navigation menu - Added configurable settings for admin headings Version 0.2.2 [2018-12-02] -------------------------- -- `#20 `_: - [qa] Added ``checkcommit`` QA check (thanks to `@ppabcd `_) +- `#20 `_: [qa] + Added ``checkcommit`` QA check (thanks to `@ppabcd + `_) Version 0.2.1 [2018-11-04] -------------------------- - `dc977d2 `_: [multitenancy] Avoid failure if org field not present -- `#13 `_: - [DRF] Added ``BaseSerializer`` -- `#16 `_: - [qa] Added migration filename check +- `#13 `_: [DRF] Added + ``BaseSerializer`` +- `#16 `_: [qa] Added + migration filename check - `babbd74 `_: [multitenancy] Added ``MultitenantAdminMixin.multitenant_parent`` - `6d45df5 `_: @@ -306,8 +338,8 @@ Version 0.2.1 [2018-11-04] Version 0.2.0 [2018-02-06] -------------------------- -- `#10 `_: - [qa] add django 2.0 compatibility +- `#10 `_: [qa] add + django 2.0 compatibility - `d742d4 `_: [version] Improved get_version to follow PEP440 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6caad925..1207222e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,5 +1,5 @@ Contributing ============ -Please refer to the `OpenWISP contributing guidelines `_. - +Please refer to the `OpenWISP contributing guidelines +`_. diff --git a/README.rst b/README.rst index 522e36ef..7eafc1b4 100644 --- a/README.rst +++ b/README.rst @@ -2,68 +2,69 @@ openwisp-utils ============== .. image:: https://github.com/openwisp/openwisp-utils/workflows/OpenWISP%20Utils%20CI%20Build/badge.svg?branch=master - :target: https://github.com/openwisp/openwisp-utils/actions?query=workflow%3A%22OpenWISP+Utils+CI+Build%22 - :alt: CI build status + :target: https://github.com/openwisp/openwisp-utils/actions?query=workflow%3A%22OpenWISP+Utils+CI+Build%22 + :alt: CI build status .. image:: https://coveralls.io/repos/github/openwisp/openwisp-utils/badge.svg :target: https://coveralls.io/github/openwisp/openwisp-utils :alt: Test coverage .. image:: https://img.shields.io/librariesio/release/github/openwisp/openwisp-utils - :target: https://libraries.io/github/openwisp/openwisp-utils#repository_dependencies - :alt: Dependency monitoring + :target: https://libraries.io/github/openwisp/openwisp-utils#repository_dependencies + :alt: Dependency monitoring .. image:: https://badge.fury.io/py/openwisp-utils.svg :target: http://badge.fury.io/py/openwisp-utils :alt: pypi .. image:: https://pepy.tech/badge/openwisp-utils - :target: https://pepy.tech/project/openwisp-utils - :alt: downloads + :target: https://pepy.tech/project/openwisp-utils + :alt: downloads .. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square - :target: https://gitter.im/openwisp/general - :alt: support chat + :target: https://gitter.im/openwisp/general + :alt: support chat .. 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 ------------- +---- -Python and Django functions, classes and settings re-used across different OpenWISP modules, -stored here with the aim of avoiding code duplication and ease maintenance. +Python and Django functions, classes and settings re-used across different +OpenWISP modules, stored here with the aim of avoiding code duplication +and ease maintenance. **Don't repeat yourself!** .. image:: https://raw.githubusercontent.com/openwisp/openwisp2-docs/master/assets/design/openwisp-logo-black.svg - :target: http://openwisp.org + :target: http://openwisp.org Current features ---------------- -* `Configurable admin theme <#using-the-admin_theme>`_ -* `OpenWISP Dashboard <#openwisp-dashboard>`_ -* `Configurable navigation menu <#main-navigation-menu>`_ -* `Improved admin filters <#admin-filters>`_ -* `OpenAPI / Swagger documentation <#openwisp_api_docs>`_ -* `Model utilities <#model-utilities>`_ -* `Storage utilities <#storage-utilities>`_ -* `Admin utilities <#admin-utilities>`_ -* `Code utilities <#code-utilities>`_ -* `Admin Theme utilities <#admin-theme-utilities>`_ -* `REST API utilities <#rest-api-utilities>`_ -* `Test utilities <#test-utilities>`_ -* `Collection of Usage Metrics <#collection-of-usage-metrics>`_ -* `Quality assurance checks <#quality-assurance-checks>`_ - ------------- +- `Configurable admin theme <#using-the-admin_theme>`_ +- `OpenWISP Dashboard <#openwisp-dashboard>`_ +- `Configurable navigation menu <#main-navigation-menu>`_ +- `Improved admin filters <#admin-filters>`_ +- `OpenAPI / Swagger documentation <#openwisp_api_docs>`_ +- `Model utilities <#model-utilities>`_ +- `Storage utilities <#storage-utilities>`_ +- `Admin utilities <#admin-utilities>`_ +- `Code utilities <#code-utilities>`_ +- `Admin Theme utilities <#admin-theme-utilities>`_ +- `REST API utilities <#rest-api-utilities>`_ +- `Test utilities <#test-utilities>`_ +- `Collection of Usage Metrics <#collection-of-usage-metrics>`_ +- `Quality assurance checks <#quality-assurance-checks>`_ + +---- .. contents:: **Table of Contents**: - :backlinks: none - :depth: 3 + :backlinks: none + :depth: 3 ------------- +---- Install stable version from pypi -------------------------------- @@ -103,75 +104,75 @@ Using the ``admin_theme`` **The admin theme requires Django >= 2.2.**. -Add ``openwisp_utils.admin_theme`` to ``INSTALLED_APPS`` in ``settings.py``: +Add ``openwisp_utils.admin_theme`` to ``INSTALLED_APPS`` in +``settings.py``: .. code-block:: python INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'openwisp_utils.admin_theme', # <----- add this + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "openwisp_utils.admin_theme", # <----- add this # add when using autocomplete filter - 'admin_auto_filters', # <----- add this - - 'django.contrib.sites', + "admin_auto_filters", # <----- add this + "django.contrib.sites", # admin - 'django.contrib.admin', + "django.contrib.admin", ] Using ``DependencyLoader`` and ``DependencyFinder`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the list of all packages extended to ``EXTENDED_APPS`` in ``settings.py``. +Add the list of all packages extended to ``EXTENDED_APPS`` in +``settings.py``. For example, if you've extended ``django_x509``: .. code-block:: python - EXTENDED_APPS = ['django_x509'] + EXTENDED_APPS = ["django_x509"] ``DependencyFinder`` -~~~~~~~~~~~~~~~~~~~~ +++++++++++++++++++++ This is a static finder which looks for static files in the ``static`` directory of the apps listed in ``settings.EXTENDED_APPS``. -Add ``openwisp_utils.staticfiles.DependencyFinder`` to ``STATICFILES_FINDERS`` -in ``settings.py``. +Add ``openwisp_utils.staticfiles.DependencyFinder`` to +``STATICFILES_FINDERS`` in ``settings.py``. .. code-block:: python STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - 'openwisp_utils.staticfiles.DependencyFinder', # <----- add this + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "openwisp_utils.staticfiles.DependencyFinder", # <----- add this ] ``DependencyLoader`` -~~~~~~~~~~~~~~~~~~~~ +++++++++++++++++++++ This is a template loader which looks for templates in the ``templates`` directory of the apps listed in ``settings.EXTENDED_APPS``. -Add ``openwisp_utils.loaders.DependencyLoader`` to -template ``loaders`` in ``settings.py`` as shown below. +Add ``openwisp_utils.loaders.DependencyLoader`` to template ``loaders`` in +``settings.py`` as shown below. .. code-block:: python TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'OPTIONS': { - 'loaders': [ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "OPTIONS": { + "loaders": [ # ... other loaders ... - 'openwisp_utils.loaders.DependencyLoader', # <----- add this + "openwisp_utils.loaders.DependencyLoader", # <----- add this ], - 'context_processors': [ + "context_processors": [ # ... omitted ... ], }, @@ -179,50 +180,55 @@ template ``loaders`` in ``settings.py`` as shown below. ] Supplying custom CSS and JS for the admin theme -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add ``openwisp_utils.admin_theme.context_processor.admin_theme_settings`` to -template ``context_processors`` in ``settings.py`` as shown below. -This will allow to set `OPENWISP_ADMIN_THEME_LINKS <#openwisp_admin_theme_links>`_ -and `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_js>`__ settings -to provide CSS and JS files to customise admin theme. +Add ``openwisp_utils.admin_theme.context_processor.admin_theme_settings`` +to template ``context_processors`` in ``settings.py`` as shown below. This +will allow to set `OPENWISP_ADMIN_THEME_LINKS +<#openwisp_admin_theme_links>`_ and `OPENWISP_ADMIN_THEME_JS +<#openwisp_admin_theme_js>`__ settings to provide CSS and JS files to +customise admin theme. .. code-block:: python TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'OPTIONS': { - 'loaders': [ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "OPTIONS": { + "loaders": [ # ... omitted ... ], - 'context_processors': [ + "context_processors": [ # ... other context processors ... - 'openwisp_utils.admin_theme.context_processor.admin_theme_settings' # <----- add this + "openwisp_utils.admin_theme.context_processor.admin_theme_settings" # <----- add this ], }, }, ] .. note:: + You will have to deploy these static files on your own. - In order to make django able to find and load these files - you may want to use the ``STATICFILES_DIR`` setting in ``settings.py``. + In order to make django able to find and load these files you may want + to use the ``STATICFILES_DIR`` setting in ``settings.py``. - You can learn more in the `Django documentation `_. + You can learn more in the `Django documentation + `_. Extend admin theme programmatically -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++++++++++++++++++++++++++++++++++++ ``openwisp_utils.admin_theme.theme.register_theme_link`` -"""""""""""""""""""""""""""""""""""""""""""""""""""""""" +........................................................ -Allows adding items to `OPENWISP_ADMIN_THEME_LINKS <#openwisp_admin_theme_links>`__. +Allows adding items to `OPENWISP_ADMIN_THEME_LINKS +<#openwisp_admin_theme_links>`__. -This function is meant to be used by third party apps or OpenWISP modules which -aim to extend the core look and feel of the OpenWISP theme (eg: add new menu icons). +This function is meant to be used by third party apps or OpenWISP modules +which aim to extend the core look and feel of the OpenWISP theme (eg: add +new menu icons). **Syntax:** @@ -230,20 +236,21 @@ aim to extend the core look and feel of the OpenWISP theme (eg: add new menu ico register_theme_link(links) -+--------------------+--------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+--------------------------------------------------------------+ -| ``links`` | (``list``) List of *link* items to be added to | -| | `OPENWISP_ADMIN_THEME_LINKS <#openwisp_admin_theme_links>`__ | -+--------------------+--------------------------------------------------------------+ +============= ============================================================ +**Parameter** **Description** +``links`` (``list``) List of *link* items to be added to + `OPENWISP_ADMIN_THEME_LINKS <#openwisp_admin_theme_links>`__ +============= ============================================================ ``openwisp_utils.admin_theme.theme.unregister_theme_link`` -"""""""""""""""""""""""""""""""""""""""""""""""""""""""""" +.......................................................... -Allows removing items from `OPENWISP_ADMIN_THEME_LINKS <#openwisp_admin_theme_links>`__. +Allows removing items from `OPENWISP_ADMIN_THEME_LINKS +<#openwisp_admin_theme_links>`__. -This function is meant to be used by third party apps or OpenWISP modules which -aim additional functionalities to UI of OpenWISP (eg: adding a support chatbot). +This function is meant to be used by third party apps or OpenWISP modules +which aim additional functionalities to UI of OpenWISP (eg: adding a +support chatbot). **Syntax:** @@ -251,17 +258,17 @@ aim additional functionalities to UI of OpenWISP (eg: adding a support chatbot). unregister_theme_link(links) -+--------------------+--------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+--------------------------------------------------------------+ -| ``links`` | (``list``) List of *link* items to be removed from | -| | `OPENWISP_ADMIN_THEME_LINKS <#openwisp_admin_theme_links>`__ | -+--------------------+--------------------------------------------------------------+ +============= ============================================================ +**Parameter** **Description** +``links`` (``list``) List of *link* items to be removed from + `OPENWISP_ADMIN_THEME_LINKS <#openwisp_admin_theme_links>`__ +============= ============================================================ ``openwisp_utils.admin_theme.theme.register_theme_js`` -"""""""""""""""""""""""""""""""""""""""""""""""""""""" +...................................................... -Allows adding items to `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_JS>`__. +Allows adding items to `OPENWISP_ADMIN_THEME_JS +<#openwisp_admin_theme_JS>`__. **Syntax:** @@ -269,17 +276,17 @@ Allows adding items to `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_JS>`__. register_theme_js(js) -+--------------------+---------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+---------------------------------------------------------------+ -| ``js`` | (``list``) List of relative path of *js* files to be added to | -| | `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_js>`__ | -+--------------------+---------------------------------------------------------------+ +============= ========================================================== +**Parameter** **Description** +``js`` (``list``) List of relative path of *js* files to be added + to `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_js>`__ +============= ========================================================== ``openwisp_utils.admin_theme.theme.unregister_theme_js`` -"""""""""""""""""""""""""""""""""""""""""""""""""""""""" +........................................................ -Allows removing items from `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_JS>`__. +Allows removing items from `OPENWISP_ADMIN_THEME_JS +<#openwisp_admin_theme_JS>`__. **Syntax:** @@ -287,43 +294,40 @@ Allows removing items from `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_JS>`_ unregister_theme_js(js) -+--------------------+--------------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+--------------------------------------------------------------------+ -| ``js`` | (``list``) List of relative path of *js* files to be removed from | -| | `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_js>`__ | -+--------------------+--------------------------------------------------------------------+ +============= ============================================================ +**Parameter** **Description** +``js`` (``list``) List of relative path of *js* files to be removed + from `OPENWISP_ADMIN_THEME_JS <#openwisp_admin_theme_js>`__ +============= ============================================================ OpenWISP Dashboard ------------------ The ``admin_theme`` sub app of this package provides an admin dashboard -for OpenWISP which can be manipulated with the functions described in -the next sections. +for OpenWISP which can be manipulated with the functions described in the +next sections. Example 1, monitoring: .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-utils/master/docs/dashboard1.png - :align: center + :align: center Example 2, controller: .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-utils/master/docs/dashboard2.png - :align: center + :align: center ``register_dashboard_template`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allows including a specific django template in the OpenWISP dashboard. -It is designed to allow the inclusion of the geographic map -shipped by +It is designed to allow the inclusion of the geographic map shipped by `OpenWISP Monitoring `_ but can be used to include any custom element in the dashboard. -**Note**: it is possible to register templates to be loaded -before or after charts using the ``after_charts`` keyword argument -(see below). +**Note**: it is possible to register templates to be loaded before or +after charts using the ``after_charts`` keyword argument (see below). **Syntax:** @@ -331,31 +335,27 @@ before or after charts using the ``after_charts`` keyword argument register_dashboard_template(position, config) -+--------------------+----------------------------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+----------------------------------------------------------------------------------+ -| ``position`` | (``int``) The position of the template. | -+--------------------+----------------------------------------------------------------------------------+ -| ``config`` | (``dict``) The configuration of the template. | -+--------------------+----------------------------------------------------------------------------------+ -| ``extra_config`` | **optional** (``dict``) Extra configuration you want to pass to custom template. | -+--------------------+----------------------------------------------------------------------------------+ -| ``after_charts`` | **optional** (``bool``) Whether the template should be loaded after dashboard | -| | charts. Defaults to ``False``, i.e. templates are loaded before dashboard | -| | charts by default. | -+--------------------+----------------------------------------------------------------------------------+ +================ ======================================================= +**Parameter** **Description** +``position`` (``int``) The position of the template. +``config`` (``dict``) The configuration of the template. +``extra_config`` **optional** (``dict``) Extra configuration you want to + pass to custom template. +``after_charts`` **optional** (``bool``) Whether the template should be + loaded after dashboard charts. Defaults to ``False``, + i.e. templates are loaded before dashboard charts by + default. +================ ======================================================= Following properties can be configured for each template ``config``: -+-----------------+------------------------------------------------------------------------------------------------------+ -| **Property** | **Description** | -+-----------------+------------------------------------------------------------------------------------------------------+ -| ``template`` | (``str``) Path to pass to the template loader. | -+-----------------+------------------------------------------------------------------------------------------------------+ -| ``css`` | (``tuple``) List of CSS files to load in the HTML page. | -+-----------------+------------------------------------------------------------------------------------------------------+ -| ``js`` | (``tuple``) List of Javascript files to load in the HTML page. | -+-----------------+------------------------------------------------------------------------------------------------------+ +============ ======================================================== +**Property** **Description** +``template`` (``str``) Path to pass to the template loader. +``css`` (``tuple``) List of CSS files to load in the HTML page. +``js`` (``tuple``) List of Javascript files to load in the HTML + page. +============ ======================================================== Code example: @@ -366,21 +366,21 @@ Code example: register_dashboard_template( position=0, config={ - 'template': 'admin/dashboard/device_map.html', - 'css': ( - 'monitoring/css/device-map.css', - 'leaflet/leaflet.css', - 'monitoring/css/leaflet.fullscreen.css', + "template": "admin/dashboard/device_map.html", + "css": ( + "monitoring/css/device-map.css", + "leaflet/leaflet.css", + "monitoring/css/leaflet.fullscreen.css", + ), + "js": ( + "monitoring/js/device-map.js", + "leaflet/leaflet.js", + "leaflet/leaflet.extras.js", + "monitoring/js/leaflet.fullscreen.min.js", ), - 'js': ( - 'monitoring/js/device-map.js', - 'leaflet/leaflet.js', - 'leaflet/leaflet.extras.js', - 'monitoring/js/leaflet.fullscreen.min.js' - ) }, extra_config={ - 'optional_variable': 'any_valid_value', + "optional_variable": "any_valid_value", }, after_charts=True, ) @@ -389,7 +389,7 @@ It is recommended to register dashboard templates from the ``ready`` method of the AppConfig of the app where the templates are defined. ``unregister_dashboard_template`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This function can be used to remove a template from the dashboard. @@ -399,11 +399,10 @@ This function can be used to remove a template from the dashboard. unregister_dashboard_template(template_name) -+-------------------+---------------------------------------------------+ -| **Parameter** | **Description** | -+-------------------+---------------------------------------------------+ -| ``template_name`` | (``str``) The name of the template to remove. | -+-------------------+---------------------------------------------------+ +================= ============================================= +**Parameter** **Description** +``template_name`` (``str``) The name of the template to remove. +================= ============================================= Code example: @@ -411,20 +410,20 @@ Code example: from openwisp_utils.admin_theme import unregister_dashboard_template - unregister_dashboard_template('admin/dashboard/device_map.html') + unregister_dashboard_template("admin/dashboard/device_map.html") -**Note**: an ``ImproperlyConfigured`` exception is raised the -specified dashboard template is not registered. +**Note**: an ``ImproperlyConfigured`` exception is raised the specified +dashboard template is not registered. ``register_dashboard_chart`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Adds a chart to the OpenWISP dashboard. At the moment only pie charts are supported. -The code works by defining the type of query which will be executed, -and optionally, how the returned values have to be colored and labeled. +The code works by defining the type of query which will be executed, and +optionally, how the returned values have to be colored and labeled. **Syntax:** @@ -432,76 +431,81 @@ and optionally, how the returned values have to be colored and labeled. register_dashboard_chart(position, config) -+--------------------+-------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+-------------------------------------------------------------+ -| ``position`` | (``int``) Position of the chart. | -+--------------------+-------------------------------------------------------------+ -| ``config`` | (``dict``) Configuration of chart. | -+--------------------+-------------------------------------------------------------+ +============= ================================== +**Parameter** **Description** +``position`` (``int``) Position of the chart. +``config`` (``dict``) Configuration of chart. +============= ================================== Following properties can be configured for each chart ``config``: -+------------------+---------------------------------------------------------------------------------------------------------+ -| **Property** | **Description** | -+------------------+---------------------------------------------------------------------------------------------------------+ -| ``query_params`` | It is a required property in form of ``dict`` containing following properties: | -| | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | **Property** | **Description** | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``name`` | (``str``) Chart title shown in the user interface. | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``app_label`` | (``str``) App label of the model that will be used to query the database. | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``model`` | (``str``) Name of the model that will be used to query the database. | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``group_by`` | (``str``) The property which will be used to group values. | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``annotate`` | Alternative to ``group_by``, ``dict`` used for more complex queries. | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``aggregate`` | Alternative to ``group_by``, ``dict`` used for more complex queries. | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``filter`` | ``dict`` used for filtering queryset. | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``organization_field`` | (``str``) If the model does not have a direct relation with the | | -| | | | ``Organization`` model, then indirect relation can be specified using | | -| | | | this property. E.g.: ``device__organization_id``. | | -| | +------------------------+---------------------------------------------------------------------------+ | -+------------------+---------------------------------------------------------------------------------------------------------+ -| ``colors`` | An **optional** ``dict`` which can be used to define colors for each distinct | -| | value shown in the pie charts. | -+------------------+---------------------------------------------------------------------------------------------------------+ -| ``labels`` | An **optional** ``dict`` which can be used to define translatable strings for each distinct | -| | value shown in the pie charts. Can be used also to provide fallback human readable values for | -| | raw values stored in the database which would be otherwise hard to understand for the user. | -+------------------+---------------------------------------------------------------------------------------------------------+ -| ``filters`` | An **optional** ``dict`` which can be used when using ``aggregate`` and ``annotate`` in | -| | ``query_params`` to define the link that will be generated to filter results (pie charts are | -| | clickable and clicking on a portion of it will show the filtered results). | -+------------------+---------------------------------------------------------------------------------------------------------+ -| ``main_filters`` | An **optional** ``dict`` which can be used to add additional filtering on the target link. | -+------------------+---------------------------------------------------------------------------------------------------------+ -| ``filtering`` | An **optional** ``str`` which can be set to ``'False'`` (str) to disable filtering on target links. | -| | This is useful when clicking on any section of the chart should take user to the same URL. | -+------------------+---------------------------------------------------------------------------------------------------------+ -| ``quick_link`` | An **optional** ``dict`` which contains configuration for the quick link button rendered | -| | below the chart. | -| | | -| | **NOTE**: The chart legend is disabled if configuration for quick link button is provided. | -| | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | **Property** | **Description** | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``url`` | (``str``) URL for the anchor tag | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``label`` | (``str``) Label shown on the button | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``title`` | (``str``) Title attribute of the button element | | -| | +------------------------+---------------------------------------------------------------------------+ | -| | | ``custom_css_classes`` | (``list``) List of CSS classes that'll be applied on the button | | -| | +------------------------+---------------------------------------------------------------------------+ | -+------------------+---------------------------------------------------------------------------------------------------------+ +================ ========================================================= +**Property** **Description** +``query_params`` It is a required property in form of ``dict`` containing + following properties: + + ====================== ================================== + **Property** **Description** + ``name`` (``str``) Chart title shown in the + user interface. + ``app_label`` (``str``) App label of the model + that will be used to query the + database. + ``model`` (``str``) Name of the model that + will be used to query the + database. + ``group_by`` (``str``) The property which will + be used to group values. + ``annotate`` Alternative to ``group_by``, + ``dict`` used for more complex + queries. + ``aggregate`` Alternative to ``group_by``, + ``dict`` used for more complex + queries. + ``filter`` ``dict`` used for filtering + queryset. + ``organization_field`` (``str``) If the model does not + have a direct relation with the + ``Organization`` model, then + indirect relation can be specified + using this property. E.g.: + ``device__organization_id``. + ====================== ================================== +``colors`` An **optional** ``dict`` which can be used to define + colors for each distinct value shown in the pie charts. +``labels`` An **optional** ``dict`` which can be used to define + translatable strings for each distinct value shown in the + pie charts. Can be used also to provide fallback human + readable values for raw values stored in the database + which would be otherwise hard to understand for the user. +``filters`` An **optional** ``dict`` which can be used when using + ``aggregate`` and ``annotate`` in ``query_params`` to + define the link that will be generated to filter results + (pie charts are clickable and clicking on a portion of it + will show the filtered results). +``main_filters`` An **optional** ``dict`` which can be used to add + additional filtering on the target link. +``filtering`` An **optional** ``str`` which can be set to ``'False'`` + (str) to disable filtering on target links. This is + useful when clicking on any section of the chart should + take user to the same URL. +``quick_link`` An **optional** ``dict`` which contains configuration for + the quick link button rendered below the chart. + + **NOTE**: The chart legend is disabled if configuration + for quick link button is provided. + + ====================== ================================ + **Property** **Description** + ``url`` (``str``) URL for the anchor tag + ``label`` (``str``) Label shown on the + button + ``title`` (``str``) Title attribute of the + button element + ``custom_css_classes`` (``list``) List of CSS classes + that'll be applied on the button + ====================== ================================ +================ ========================================================= Code example: @@ -512,37 +516,37 @@ Code example: register_dashboard_chart( position=1, config={ - 'query_params': { - 'name': 'Operator Project Distribution', - 'app_label': 'test_project', - 'model': 'operator', - 'group_by': 'project__name', + "query_params": { + "name": "Operator Project Distribution", + "app_label": "test_project", + "model": "operator", + "group_by": "project__name", }, - 'colors': {'Utils': 'red', 'User': 'orange'}, - 'quick_link': { - 'url': '/admin/test_project/operator', - 'label': 'Open Operators list', - 'title': 'View complete list of operators', - 'custom_css_classes': ['negative-top-20'], + "colors": {"Utils": "red", "User": "orange"}, + "quick_link": { + "url": "/admin/test_project/operator", + "label": "Open Operators list", + "title": "View complete list of operators", + "custom_css_classes": ["negative-top-20"], }, }, ) -For real world examples, look at the code of -`OpenWISP Controller `__ -and `OpenWISP Monitoring `_. +For real world examples, look at the code of `OpenWISP Controller +`__ and `OpenWISP +Monitoring `_. -**Note**: an ``ImproperlyConfigured`` exception is raised if a -dashboard element is already registered at same position. +**Note**: an ``ImproperlyConfigured`` exception is raised if a dashboard +element is already registered at same position. It is recommended to register dashboard charts from the ``ready`` method -of the AppConfig of the app where the models are defined. -Checkout `app.py of the test_project +of the AppConfig of the app where the models are defined. Checkout `app.py +of the test_project `_ for reference. ``unregister_dashboard_chart`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This function can used to remove a chart from the dashboard. @@ -552,11 +556,10 @@ This function can used to remove a chart from the dashboard. unregister_dashboard_chart(chart_name) -+------------------+---------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+---------------------------------------------------+ -| ``chart_name`` | (``str``) The name of the chart to remove. | -+------------------+---------------------------------------------------+ +============== ========================================== +**Parameter** **Description** +``chart_name`` (``str``) The name of the chart to remove. +============== ========================================== Code example: @@ -564,16 +567,16 @@ Code example: from openwisp_utils.admin_theme import unregister_dashboard_chart - unregister_dashboard_chart('Operator Project Distribution') + unregister_dashboard_chart("Operator Project Distribution") -**Note**: an ``ImproperlyConfigured`` exception is raised the -specified dashboard chart is not registered. +**Note**: an ``ImproperlyConfigured`` exception is raised the specified +dashboard chart is not registered. Main navigation menu -------------------- -The ``admin_theme`` sub app of this package provides a navigation menu that can be -manipulated with the functions described in the next sections. +The ``admin_theme`` sub app of this package provides a navigation menu +that can be manipulated with the functions described in the next sections. Add ``openwisp_utils.admin_theme.context_processor.menu_groups`` to template ``context_processors`` in ``settings.py`` as shown below. @@ -582,24 +585,25 @@ template ``context_processors`` in ``settings.py`` as shown below. TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'OPTIONS': { - 'loaders': [ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "OPTIONS": { + "loaders": [ # ... omitted ... ], - 'context_processors': [ + "context_processors": [ # ... other context processors ... - 'openwisp_utils.admin_theme.context_processor.menu_groups' # <----- add this + "openwisp_utils.admin_theme.context_processor.menu_groups" # <----- add this ], }, }, ] ``register_menu_group`` -^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~ -Allows registering a new menu item or group at the specified position in the Main Navigation Menu. +Allows registering a new menu item or group at the specified position in +the Main Navigation Menu. **Syntax:** @@ -607,13 +611,11 @@ Allows registering a new menu item or group at the specified position in the Mai register_menu_group(position, config) -+--------------------+-------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+-------------------------------------------------------------+ -| ``position`` | (``int``) Position of the group or item. | -+--------------------+-------------------------------------------------------------+ -| ``config`` | (``dict``) Configuration of the goup or item. | -+--------------------+-------------------------------------------------------------+ +============= ============================================= +**Parameter** **Description** +``position`` (``int``) Position of the group or item. +``config`` (``dict``) Configuration of the goup or item. +============= ============================================= Code example: @@ -625,51 +627,57 @@ Code example: register_menu_group( position=1, config={ - 'label': _('My Group'), - 'items': { + "label": _("My Group"), + "items": { 1: { - 'label': _('Users List'), - 'model': 'auth.User', - 'name': 'changelist', - 'icon': 'list-icon', + "label": _("Users List"), + "model": "auth.User", + "name": "changelist", + "icon": "list-icon", }, 2: { - 'label': _('Add User'), - 'model': 'auth.User', - 'name': 'add', - 'icon': 'add-icon', + "label": _("Add User"), + "model": "auth.User", + "name": "add", + "icon": "add-icon", }, }, - 'icon': 'user-group-icon', + "icon": "user-group-icon", }, ) register_menu_group( position=2, config={ - 'model': 'test_project.Shelf', - 'name': 'changelist', - 'label': _('View Shelf'), - 'icon': 'shelf-icon', + "model": "test_project.Shelf", + "name": "changelist", + "label": _("View Shelf"), + "icon": "shelf-icon", }, ) register_menu_group( - position=3, config={'label': _('My Link'), 'url': 'https://link.com'} + position=3, config={"label": _("My Link"), "url": "https://link.com"} ) .. note:: - An ``ImproperlyConfigured`` exception is raised if a menu element is already registered at the same position. - An ``ImproperlyConfigured`` exception is raised if the supplied configuration does not match with the different types of - possible configurations available (different configurations will be discussed in the next section). + An ``ImproperlyConfigured`` exception is raised if a menu element is + already registered at the same position. + + An ``ImproperlyConfigured`` exception is raised if the supplied + configuration does not match with the different types of possible + configurations available (different configurations will be discussed + in the next section). - It is recommended to use ``register_menu_group`` in the ``ready`` method of the ``AppConfig``. + It is recommended to use ``register_menu_group`` in the ``ready`` + method of the ``AppConfig``. - ``register_menu_items`` is obsoleted by ``register_menu_group`` and will be removed in - future versions. Links added using ``register_menu_items`` will be shown at the top - of navigation menu and above any ``register_menu_group`` items. + ``register_menu_items`` is obsoleted by ``register_menu_group`` and + will be removed in future versions. Links added using + ``register_menu_items`` will be shown at the top of navigation menu + and above any ``register_menu_group`` items. Adding a custom link -~~~~~~~~~~~~~~~~~~~~~ +++++++++++++++++++++ To add a link that contains a custom URL the following syntax can be used. @@ -677,31 +685,27 @@ To add a link that contains a custom URL the following syntax can be used. .. code-block:: python - register_menu_group(position=1, config={ - "label": "Link Label", - "url": "link_url", - "icon": "my-icon" - }) + register_menu_group( + position=1, + config={"label": "Link Label", "url": "link_url", "icon": "my-icon"}, + ) Following is the description of the configuration: -+------------------+--------------------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+--------------------------------------------------------------+ -| ``label`` | (``str``) Display text for the link. | -+------------------+--------------------------------------------------------------+ -| ``url`` | (``str``) url for the link. | -+------------------+--------------------------------------------------------------+ -| ``icon`` | An **optional** ``str`` CSS class name for the icon. No icon | -| | is displayed if not provided. | -+------------------+--------------------------------------------------------------+ +============= ============================================================ +**Parameter** **Description** +``label`` (``str``) Display text for the link. +``url`` (``str``) url for the link. +``icon`` An **optional** ``str`` CSS class name for the icon. No icon + is displayed if not provided. +============= ============================================================ Adding a model link -~~~~~~~~~~~~~~~~~~~ ++++++++++++++++++++ To add a link that contains URL of add form or change list page of a model -then following syntax can be used. Users will only be able to see links for -models they have permission to either view or edit. +then following syntax can be used. Users will only be able to see links +for models they have permission to either view or edit. **Syntax:** @@ -711,10 +715,10 @@ models they have permission to either view or edit. register_menu_group( position=1, config={ - 'model': 'my_project.MyModel', - 'name': 'changelist', - 'label': 'MyModel List', - 'icon': 'my-model-list-class', + "model": "my_project.MyModel", + "name": "changelist", + "label": "MyModel List", + "icon": "my-model-list-class", }, ) @@ -722,34 +726,30 @@ models they have permission to either view or edit. register_menu_group( position=2, config={ - 'model': 'my_project.MyModel', - 'name': 'add', - 'label': 'MyModel Add Item', - 'icon': 'my-model-add-class', + "model": "my_project.MyModel", + "name": "add", + "label": "MyModel Add Item", + "icon": "my-model-add-class", }, ) Following is the description of the configuration: -+------------------+--------------------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+--------------------------------------------------------------+ -| ``model`` | (``str``) Model of the app for which you to add link. | -+------------------+--------------------------------------------------------------+ -| ``name`` | (``str``) url name. eg. changelist or add. | -+------------------+--------------------------------------------------------------+ -| ``label`` | An **optional** ``str`` display text for the link. It is | -| | automatically generated if not provided. | -+------------------+--------------------------------------------------------------+ -| ``icon`` | An **optional** ``str`` CSS class name for the icon. No icon | -| | is displayed if not provided. | -+------------------+--------------------------------------------------------------+ +============= ============================================================ +**Parameter** **Description** +``model`` (``str``) Model of the app for which you to add link. +``name`` (``str``) url name. eg. changelist or add. +``label`` An **optional** ``str`` display text for the link. It is + automatically generated if not provided. +``icon`` An **optional** ``str`` CSS class name for the icon. No icon + is displayed if not provided. +============= ============================================================ Adding a menu group -~~~~~~~~~~~~~~~~~~~ ++++++++++++++++++++ -To add a nested group of links in the menu the following syntax can be used. -It creates a dropdown in the menu. +To add a nested group of links in the menu the following syntax can be +used. It creates a dropdown in the menu. **Syntax:** @@ -758,37 +758,38 @@ It creates a dropdown in the menu. register_menu_group( position=1, config={ - 'label': 'My Group Label', - 'items': { - 1: {'label': 'Link Label', 'url': 'link_url', 'icon': 'my-icon'}, + "label": "My Group Label", + "items": { + 1: { + "label": "Link Label", + "url": "link_url", + "icon": "my-icon", + }, 2: { - 'model': 'my_project.MyModel', - 'name': 'changelist', - 'label': 'MyModel List', - 'icon': 'my-model-list-class', + "model": "my_project.MyModel", + "name": "changelist", + "label": "MyModel List", + "icon": "my-model-list-class", }, }, - 'icon': 'my-group-icon-class', + "icon": "my-group-icon-class", }, ) Following is the description of the configuration: -+------------------+--------------------------------------------------------------+ -| **Parameter** | **Description** | -+------------------+--------------------------------------------------------------+ -| ``label`` | (``str``) Display name for the link. | -+------------------+--------------------------------------------------------------+ -| ``items`` | (``dict``) Items to be displayed in the dropdown. | -| | It can be a dict of custom links or model links | -| | with key as their position in the group. | -+------------------+--------------------------------------------------------------+ -| ``icon`` | An **optional** ``str`` CSS class name for the icon. No icon | -| | is displayed if not provided. | -+------------------+--------------------------------------------------------------+ +============= ============================================================ +**Parameter** **Description** +``label`` (``str``) Display name for the link. +``items`` (``dict``) Items to be displayed in the dropdown. It can be + a dict of custom links or model links with key as their + position in the group. +``icon`` An **optional** ``str`` CSS class name for the icon. No icon + is displayed if not provided. +============= ============================================================ ``register_menu_subitem`` -^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~ Allows adding an item to a registered group. @@ -798,15 +799,14 @@ Allows adding an item to a registered group. register_menu_subitem(group_position, item_position, config) -+--------------------------+----------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------------+----------------------------------------------------------------+ -| ``group_position`` | (``int``) Position of the group in which item should be added. | -+--------------------------+----------------------------------------------------------------+ -| ``item_position`` | (``int``) Position at which item should be added in the group | -+--------------------------+----------------------------------------------------------------+ -| ``config`` | (``dict``) Configuration of the item. | -+--------------------------+----------------------------------------------------------------+ +================== ======================================================= +**Parameter** **Description** +``group_position`` (``int``) Position of the group in which item should be + added. +``item_position`` (``int``) Position at which item should be added in the + group +``config`` (``dict``) Configuration of the item. +================== ======================================================= Code example: @@ -820,10 +820,10 @@ Code example: group_position=10, item_position=2, config={ - 'label': _('Users List'), - 'model': 'auth.User', - 'name': 'changelist', - 'icon': 'list-icon', + "label": _("Users List"), + "model": "auth.User", + "name": "changelist", + "icon": "list-icon", }, ) @@ -831,30 +831,31 @@ Code example: register_menu_subitem( group_position=10, item_position=2, - config={'label': _('My Link'), 'url': 'https://link.com'}, + config={"label": _("My Link"), "url": "https://link.com"}, ) .. note:: - An ``ImproperlyConfigured`` exception is raised if the group is not already - registered at ``group_position``. - An ``ImproperlyConfigured`` exception is raised if the group already has an - item registered at ``item_position``. + An ``ImproperlyConfigured`` exception is raised if the group is not + already registered at ``group_position``. - It is only possible to register links to specific models or custom URL. - An ``ImproperlyConfigured`` exception is raised if the configuration of - group is provided in the function. + An ``ImproperlyConfigured`` exception is raised if the group already + has an item registered at ``item_position``. + + It is only possible to register links to specific models or custom + URL. An ``ImproperlyConfigured`` exception is raised if the + configuration of group is provided in the function. It is recommended to use ``register_menu_subitem`` in the ``ready`` method of the ``AppConfig``. How to use custom icons in the menu -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Create a CSS file and use the following syntax to provide the image for each -icon used in the menu. The CSS class name should be the same as the ``icon`` -parameter used in the configuration of a menu item or group. Also icon being used -should be in ``svg`` format. +Create a CSS file and use the following syntax to provide the image for +each icon used in the menu. The CSS class name should be the same as the +``icon`` parameter used in the configuration of a menu item or group. Also +icon being used should be in ``svg`` format. Example: @@ -865,48 +866,51 @@ Example: -webkit-mask-image: url(imageurl); } -Follow the instructions in -`Supplying custom CSS and JS for the admin theme <#supplying-custom-css-and-js-for-the-admin-theme>`_ -to know how to configure your OpenWISP instance to load custom CSS files. +Follow the instructions in `Supplying custom CSS and JS for the admin +theme <#supplying-custom-css-and-js-for-the-admin-theme>`_ to know how to +configure your OpenWISP instance to load custom CSS files. Admin filters ------------- .. figure:: https://github.com/openwisp/openwisp-utils/raw/media/docs/filter.gif - :align: center + :align: center -The ``admin_theme`` sub app provides an improved UI for the changelist filter -which occupies less space compared to the original implementation in django: -filters are displayed horizontally on the top (instead of vertically on the side) -and filter options are hidden in dropdown menus which are expanded once clicked. +The ``admin_theme`` sub app provides an improved UI for the changelist +filter which occupies less space compared to the original implementation +in django: filters are displayed horizontally on the top (instead of +vertically on the side) and filter options are hidden in dropdown menus +which are expanded once clicked. -Multiple filters can be applied at same time with the help of "apply filter" button. -This button is only visible when total number of filters is greater than 4. -When filters in use are less or equal to 4 the "apply filter" button is not visible -and filters work like in the original django implementation -(as soon as a filter option is selected the filter is applied and the page is reloaded). +Multiple filters can be applied at same time with the help of "apply +filter" button. This button is only visible when total number of filters +is greater than 4. When filters in use are less or equal to 4 the "apply +filter" button is not visible and filters work like in the original django +implementation (as soon as a filter option is selected the filter is +applied and the page is reloaded). Model utilities --------------- ``openwisp_utils.base.UUIDModel`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Model class which provides a UUID4 primary key. ``openwisp_utils.base.TimeStampedEditableModel`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Model class inheriting ``UUIDModel`` which provides two additional fields: - ``created`` - ``modified`` -Which use respectively ``AutoCreatedField``, ``AutoLastModifiedField`` from ``model_utils.fields`` -(self-updating fields providing the creation date-time and the last modified date-time). +Which use respectively ``AutoCreatedField``, ``AutoLastModifiedField`` +from ``model_utils.fields`` (self-updating fields providing the creation +date-time and the last modified date-time). ``openwisp_utils.base.FallBackModelMixin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Model mixin that implements ``get_field_value`` method which can be used to get value of fallback fields. @@ -918,19 +922,23 @@ This section describes custom fields defined in ``openwisp_utils.fields`` that can be used in Django models: ``openwisp_utils.fields.KeyField`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A model field which provides a random key or token, widely used across openwisp modules. +A model field which provides a random key or token, widely used across +openwisp modules. ``openwisp_utils.fields.FallbackBooleanChoiceField`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This field extends Django's `BooleanField `_ -and provides additional functionality for handling choices with a fallback value. -The field will use the **fallback value** whenever the field is set to ``None``. +This field extends Django's `BooleanField +`_ +and provides additional functionality for handling choices with a fallback +value. The field will use the **fallback value** whenever the field is set +to ``None``. -This field is particularly useful when you want to present a choice between enabled -and disabled options, with an additional "Default" option that reflects the fallback value. +This field is particularly useful when you want to present a choice +between enabled and disabled options, with an additional "Default" option +that reflects the fallback value. .. code-block:: python @@ -938,6 +946,7 @@ and disabled options, with an additional "Default" option that reflects the fall from openwisp_utils.fields import FallbackBooleanChoiceField from myapp import settings as app_settings + class MyModel(models.Model): is_active = FallbackBooleanChoiceField( null=True, @@ -947,11 +956,13 @@ and disabled options, with an additional "Default" option that reflects the fall ) ``openwisp_utils.fields.FallbackCharChoiceField`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This field extends Django's `CharField `_ -and provides additional functionality for handling choices with a fallback value. -The field will use the **fallback value** whenever the field is set to ``None``. +This field extends Django's `CharField +`_ and +provides additional functionality for handling choices with a fallback +value. The field will use the **fallback value** whenever the field is set +to ``None``. .. code-block:: python @@ -959,26 +970,30 @@ The field will use the **fallback value** whenever the field is set to ``None``. from openwisp_utils.fields import FallbackCharChoiceField from myapp import settings as app_settings + class MyModel(models.Model): is_first_name_required = FallbackCharChoiceField( null=True, blank=True, max_length=32, choices=( - ('disabled', _('Disabled')), - ('allowed', _('Allowed')), - ('mandatory', _('Mandatory')), + ("disabled", _("Disabled")), + ("allowed", _("Allowed")), + ("mandatory", _("Mandatory")), ), fallback=app_settings.IS_FIRST_NAME_REQUIRED, ) ``openwisp_utils.fields.FallbackCharField`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This field extends Django's `CharField `_ -and provides additional functionality for handling text fields with a fallback value. +This field extends Django's `CharField +`_ and +provides additional functionality for handling text fields with a fallback +value. -It allows populating the form with the fallback value when the actual value is set to ``null`` in the database. +It allows populating the form with the fallback value when the actual +value is set to ``null`` in the database. .. code-block:: python @@ -986,6 +1001,7 @@ It allows populating the form with the fallback value when the actual value is s from openwisp_utils.fields import FallbackCharField from myapp import settings as app_settings + class MyModel(models.Model): greeting_text = FallbackCharField( null=True, @@ -995,12 +1011,15 @@ It allows populating the form with the fallback value when the actual value is s ) ``openwisp_utils.fields.FallbackURLField`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This field extends Django's `URLField `_ -and provides additional functionality for handling URL fields with a fallback value. +This field extends Django's `URLField +`_ and +provides additional functionality for handling URL fields with a fallback +value. -It allows populating the form with the fallback value when the actual value is set to ``null`` in the database. +It allows populating the form with the fallback value when the actual +value is set to ``null`` in the database. .. code-block:: python @@ -1008,6 +1027,7 @@ It allows populating the form with the fallback value when the actual value is s from openwisp_utils.fields import FallbackURLField from myapp import settings as app_settings + class MyModel(models.Model): password_reset_url = FallbackURLField( null=True, @@ -1017,12 +1037,15 @@ It allows populating the form with the fallback value when the actual value is s ) ``openwisp_utils.fields.FallbackTextField`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This extends Django's `TextField `_ -and provides additional functionality for handling text fields with a fallback value. +This extends Django's `TextField +`_ +and provides additional functionality for handling text fields with a +fallback value. -It allows populating the form with the fallback value when the actual value is set to ``null`` in the database. +It allows populating the form with the fallback value when the actual +value is set to ``null`` in the database. .. code-block:: python @@ -1030,6 +1053,7 @@ It allows populating the form with the fallback value when the actual value is s from openwisp_utils.fields import FallbackTextField from myapp import settings as app_settings + class MyModel(models.Model): extra_config = FallbackTextField( null=True, @@ -1039,12 +1063,15 @@ It allows populating the form with the fallback value when the actual value is s ) ``openwisp_utils.fields.FallbackPositiveIntegerField`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This extends Django's `PositiveIntegerField `_ -and provides additional functionality for handling positive integer fields with a fallback value. +This extends Django's `PositiveIntegerField +`_ +and provides additional functionality for handling positive integer fields +with a fallback value. -It allows populating the form with the fallback value when the actual value is set to ``null`` in the database. +It allows populating the form with the fallback value when the actual +value is set to ``null`` in the database. .. code-block:: python @@ -1052,6 +1079,7 @@ It allows populating the form with the fallback value when the actual value is s from openwisp_utils.fields import FallbackPositiveIntegerField from myapp import settings as app_settings + class MyModel(models.Model): count = FallbackPositiveIntegerField( blank=True, @@ -1063,7 +1091,7 @@ Admin utilities --------------- ``openwisp_utils.admin.TimeReadonlyAdminMixin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Admin mixin which adds two readonly fields ``created`` and ``modified``. @@ -1071,64 +1099,66 @@ This is an admin mixin for models inheriting ``TimeStampedEditableModel`` which adds the fields ``created`` and ``modified`` to the database. ``openwisp_utils.admin.ReadOnlyAdmin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A read-only ``ModelAdmin`` base class. -Will include the ``id`` field by default, which can be excluded by supplying -the ``exclude`` attribute, eg: +Will include the ``id`` field by default, which can be excluded by +supplying the ``exclude`` attribute, eg: .. code-block:: python from openwisp_utils.admin import ReadOnlyAdmin + class PostAuthReadOnlyAdmin(ReadOnlyAdmin): - exclude = ['id'] + exclude = ["id"] ``openwisp_utils.admin.AlwaysHasChangedMixin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A mixin designed for inline items and model forms, ensures the item -is created even if the default values are unchanged. +A mixin designed for inline items and model forms, ensures the item is +created even if the default values are unchanged. Without this, when creating new objects, inline items won't be saved unless users change the default values. ``openwisp_utils.admin.CopyableFieldsAdmin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An admin class that allows to set admin fields to be -read-only and makes it easy to copy the fields contents. +An admin class that allows to set admin fields to be read-only and makes +it easy to copy the fields contents. Useful for auto-generated fields such as UUIDs, secret keys, tokens, etc. ``openwisp_utils.admin.UUIDAdmin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This class is a subclass of ``CopyableFieldsAdmin`` which -sets ``uuid`` as the only copyable field. This class is kept -for backward compatibility and convenience, since different models -of various OpenWISP modules show ``uuid`` as the only copyable field. +This class is a subclass of ``CopyableFieldsAdmin`` which sets ``uuid`` as +the only copyable field. This class is kept for backward compatibility and +convenience, since different models of various OpenWISP modules show +``uuid`` as the only copyable field. ``openwisp_utils.admin.ReceiveUrlAdmin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An admin class that provides an URL as a read-only input field -(to make it easy and quick to copy/paste). +An admin class that provides an URL as a read-only input field (to make it +easy and quick to copy/paste). ``openwisp_utils.admin.HelpTextStackedInline`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. figure:: https://github.com/openwisp/openwisp-utils/raw/media/docs/help-text-stacked-inline.png - :align: center + :align: center -A stacked inline admin class that displays a help text for entire -inline object. Following is an example: +A stacked inline admin class that displays a help text for entire inline +object. Following is an example: .. code-block:: python from openwisp_utils.admin import HelpTextStackedInline + class SubnetDivisionRuleInlineAdmin( MultitenantAdminMixin, TimeReadonlyAdminMixin, HelpTextStackedInline ): @@ -1136,25 +1166,25 @@ inline object. Following is an example: # It is required to set "help_text" attribute help_text = { # (required) Help text to display - 'text': _( - 'Please keep in mind that once the subnet division rule is created ' + "text": _( + "Please keep in mind that once the subnet division rule is created " 'and used, changing "Size" and "Number of Subnets" and decreasing ' '"Number of IPs" will not be possible.' ), # (optional) You can provide a link to documentation for user reference - 'documentation_url': ( - 'https://github.com/openwisp/openwisp-utils' + "documentation_url": ( + "https://github.com/openwisp/openwisp-utils" ), # (optional) Icon to be shown along with help text. By default it uses # "/static/admin/img/icon-alert.svg" - 'image_url': '/static/admin/img/icon-alert.svg' + "image_url": "/static/admin/img/icon-alert.svg", } ``openwisp_utils.admin_theme.filters.InputFilter`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``admin_theme`` sub app of this package provides an input filter that can be used in changelist page -to filter ``UUIDField`` or ``CharField``. +The ``admin_theme`` sub app of this package provides an input filter that +can be used in changelist page to filter ``UUIDField`` or ``CharField``. Code example: @@ -1164,16 +1194,18 @@ Code example: from openwisp_utils.admin_theme.filters import InputFilter from my_app.models import MyModel + @admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin): list_filter = [ - ('my_field', InputFilter), - 'other_field', + ("my_field", InputFilter), + "other_field", # ... ] -By default ``InputFilter`` use exact lookup to filter items which matches to the value being -searched by the user. But this behavior can be changed by modifying ``InputFilter`` as following: +By default ``InputFilter`` use exact lookup to filter items which matches +to the value being searched by the user. But this behavior can be changed +by modifying ``InputFilter`` as following: .. code-block:: python @@ -1181,29 +1213,33 @@ searched by the user. But this behavior can be changed by modifying ``InputFilte from openwisp_utils.admin_theme.filters import InputFilter from my_app.models import MyModel + class MyInputFilter(InputFilter): - lookup = 'icontains' + lookup = "icontains" @admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin): list_filter = [ - ('my_field', MyInputFilter), - 'other_field', + ("my_field", MyInputFilter), + "other_field", # ... ] -To know about other lookups that can be used please check -`Django Lookup API Reference `__ +To know about other lookups that can be used please check `Django Lookup +API Reference +`__ ``openwisp_utils.admin_theme.filters.SimpleInputFilter`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A stripped down version of ``openwisp_utils.admin_theme.filters.InputFilter`` that provides -flexibility to customize filtering. It can be used to filter objects using indirectly -related fields. +A stripped down version of +``openwisp_utils.admin_theme.filters.InputFilter`` that provides +flexibility to customize filtering. It can be used to filter objects using +indirectly related fields. -The derived filter class should define the ``queryset`` method as shown in following example: +The derived filter class should define the ``queryset`` method as shown in +following example: .. code-block:: python @@ -1211,9 +1247,10 @@ The derived filter class should define the ``queryset`` method as shown in follo from openwisp_utils.admin_theme.filters import SimpleInputFilter from my_app.models import MyModel + class MyInputFilter(SimpleInputFilter): - parameter_name = 'shelf' - title = _('Shelf') + parameter_name = "shelf" + title = _("Shelf") def queryset(self, request, queryset): if self.value() is not None: @@ -1224,18 +1261,19 @@ The derived filter class should define the ``queryset`` method as shown in follo class MyModelAdmin(admin.ModelAdmin): list_filter = [ MyInputFilter, - 'other_field', + "other_field", # ... ] ``openwisp_utils.admin_theme.filters.AutocompleteFilter`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``admin_theme`` sub app of this package provides an auto complete -filter that uses django autocomplete widget to load filter data asynchronously. +filter that uses django autocomplete widget to load filter data +asynchronously. -This filter can be helpful when the number of objects is too large -to load all at once which may cause the slow loading of the page. +This filter can be helpful when the number of objects is too large to load +all at once which may cause the slow loading of the page. .. code-block:: python @@ -1243,21 +1281,21 @@ to load all at once which may cause the slow loading of the page. from openwisp_utils.admin_theme.filters import AutocompleteFilter from my_app.models import MyModel, MyOtherModel + class MyAutoCompleteFilter(AutocompleteFilter): - field_name = 'field' - parameter_name = 'field_id' - title = _('My Field') + field_name = "field" + parameter_name = "field_id" + title = _("My Field") + @admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin): - list_filter = [ - MyAutoCompleteFilter, - ... - ] + list_filter = [MyAutoCompleteFilter, ...] + @admin.register(MyOtherModel) class MyOtherModelAdmin(admin.ModelAdmin): - search_fields = ['id'] + search_fields = ["id"] To customize or know more about it, please refer to the `django-admin-autocomplete-filter documentation @@ -1267,15 +1305,16 @@ Code utilities -------------- ``openwisp_utils.utils.get_random_key`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Generates an random string of 32 characters. ``openwisp_utils.utils.deep_merge_dicts`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Returns a new ``dict`` which is the result of the merge of the two dictionaries, -all elements are deep-copied to avoid modifying the original data structures. +Returns a new ``dict`` which is the result of the merge of the two +dictionaries, all elements are deep-copied to avoid modifying the original +data structures. Usage: @@ -1286,11 +1325,11 @@ Usage: mergd_dict = deep_merge_dicts(dict1, dict2) ``openwisp_utils.utils.default_or_test`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If the program is being executed during automated tests the value supplied in -the ``test`` argument will be returned, otherwise the one supplied in the -``value`` argument is returned. +If the program is being executed during automated tests the value supplied +in the ``test`` argument will be returned, otherwise the one supplied in +the ``value`` argument is returned. .. code-block:: python @@ -1298,14 +1337,15 @@ the ``test`` argument will be returned, otherwise the one supplied in the THROTTLE_RATE = getattr( settings, - 'THROTTLE_RATE', - default_or_test(value='20/day', test=None), + "THROTTLE_RATE", + default_or_test(value="20/day", test=None), ) ``openwisp_utils.utils.print_color`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**default colors**: ``['white_bold', 'green_bold', 'yellow_bold', 'red_bold']`` +**default colors**: ``['white_bold', 'green_bold', 'yellow_bold', +'red_bold']`` If you want to print a string in ``Red Bold``, you can do it as below. @@ -1313,24 +1353,25 @@ If you want to print a string in ``Red Bold``, you can do it as below. from openwisp_utils.utils import print_color - print_color('This is the printed in Red Bold', color_name='red_bold') + print_color("This is the printed in Red Bold", color_name="red_bold") -You may also provide the ``end`` arguement similar to built-in print method. +You may also provide the ``end`` arguement similar to built-in print +method. ``openwisp_utils.utils.SorrtedOrderedDict`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Extends ``collections.SortedDict`` and implements logic to sort inserted items based on ``key`` value. Sorting is done at insert operation which incurs memory space overhead. ``openwisp_utils.tasks.OpenwispCeleryTask`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A custom celery task class that sets hard and soft time limits of celery tasks -using `OPENWISP_CELERY_HARD_TIME_LIMIT <#openwisp_celery_hard_time_limit>`_ -and `OPENWISP_CELERY_SOFT_TIME_LIMIT <#openwisp_celery_soft_time_limit>`_ -settings respectively. +A custom celery task class that sets hard and soft time limits of celery +tasks using `OPENWISP_CELERY_HARD_TIME_LIMIT +<#openwisp_celery_hard_time_limit>`_ and `OPENWISP_CELERY_SOFT_TIME_LIMIT +<#openwisp_celery_soft_time_limit>`_ settings respectively. Usage: @@ -1340,22 +1381,23 @@ Usage: from openwisp_utils.tasks import OpenwispCeleryTask + @shared_task(base=OpenwispCeleryTask) def your_celery_task(): pass -**Note:** This task class should be used for regular background tasks -but not for complex background tasks which can take a long time to execute +**Note:** This task class should be used for regular background tasks but +not for complex background tasks which can take a long time to execute (eg: firmware upgrades, network operations with retry mechanisms). ``openwisp_utils.utils.retryable_request`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A utility function for making HTTP requests with built-in retry logic. -This function is useful for handling transient errors encountered during HTTP -requests by automatically retrying failed requests with exponential backoff. -It provides flexibility in configuring various retry parameters to suit -different use cases. +This function is useful for handling transient errors encountered during +HTTP requests by automatically retrying failed requests with exponential +backoff. It provides flexibility in configuring various retry parameters +to suit different use cases. Usage: @@ -1364,16 +1406,24 @@ Usage: from openwisp_utils.utils import retryable_request response = retryable_request( - method='GET', - url='https://openwisp.org', + method="GET", + url="https://openwisp.org", timeout=(4, 8), max_retries=3, backoff_factor=1, backoff_jitter=0.0, status_forcelist=(429, 500, 502, 503, 504), - allowed_methods=('HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'POST'), + allowed_methods=( + "HEAD", + "GET", + "PUT", + "DELETE", + "OPTIONS", + "TRACE", + "POST", + ), retry_kwargs=None, - headers={'Authorization': 'Bearer token'} + headers={"Authorization": "Bearer token"}, ) **Paramters:** @@ -1386,50 +1436,58 @@ Usage: request failure (default: 3). - ``backoff_factor`` (float): A factor by which the retry delay increases after each retry (default: 1). -- ``backoff_jitter`` (float): A jitter to apply to the backoff factor to prevent - retry storms (default: 0.0). -- ``status_forcelist`` (tuple): A tuple of HTTP status codes for which retries - should be attempted (default: (429, 500, 502, 503, 504)). -- ``allowed_methods`` (tuple): A tuple of HTTP methods that are allowed for - the request (default: ('HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'POST')). -- ``retry_kwargs`` (dict): Additional keyword arguments to be passed to the - retry mechanism (default: None). -- ``**kwargs``: Additional keyword arguments to be passed to the underlying request - method (e.g. 'headers', etc.). - -Note: This method will raise a requests.exceptions.RetryError if the request -remains unsuccessful even after all retry attempts have been exhausted. -This exception indicates that the operation could not be completed successfully -despite the retry mechanism. +- ``backoff_jitter`` (float): A jitter to apply to the backoff factor to + prevent retry storms (default: 0.0). +- ``status_forcelist`` (tuple): A tuple of HTTP status codes for which + retries should be attempted (default: (429, 500, 502, 503, 504)). +- ``allowed_methods`` (tuple): A tuple of HTTP methods that are allowed + for the request (default: ('HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', + 'TRACE', 'POST')). +- ``retry_kwargs`` (dict): Additional keyword arguments to be passed to + the retry mechanism (default: None). +- ``**kwargs``: Additional keyword arguments to be passed to the + underlying request method (e.g. 'headers', etc.). + +Note: This method will raise a requests.exceptions.RetryError if the +request remains unsuccessful even after all retry attempts have been +exhausted. This exception indicates that the operation could not be +completed successfully despite the retry mechanism. Storage utilities ----------------- ``openwisp_utils.storage.CompressStaticFilesStorage`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A static storage backend for compression inheriting from `django-compress-staticfiles's `_ ``CompressStaticFilesStorage`` class. +A static storage backend for compression inheriting from +`django-compress-staticfiles's +`_ +``CompressStaticFilesStorage`` class. -Adds support for excluding file types using `OPENWISP_STATICFILES_VERSIONED_EXCLUDE <#openwisp_staticfiles_versioned_exclude>`_ setting. +Adds support for excluding file types using +`OPENWISP_STATICFILES_VERSIONED_EXCLUDE +<#openwisp_staticfiles_versioned_exclude>`_ setting. -To use point ``STATICFILES_STORAGE`` to ``openwisp_utils.storage.CompressStaticFilesStorage`` in ``settings.py``. +To use point ``STATICFILES_STORAGE`` to +``openwisp_utils.storage.CompressStaticFilesStorage`` in ``settings.py``. .. code-block:: python - STATICFILES_STORAGE = 'openwisp_utils.storage.CompressStaticFilesStorage' + STATICFILES_STORAGE = "openwisp_utils.storage.CompressStaticFilesStorage" Admin Theme utilities --------------------- ``openwisp_utils.admin_theme.email.send_email`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This function allows sending email in both plain text and HTML version (using the template -and logo that can be customised using `OPENWISP_EMAIL_TEMPLATE <#openwisp_email_template>`_ -and `OPENWISP_EMAIL_LOGO <#openwisp_email_logo>`_ respectively). +This function allows sending email in both plain text and HTML version +(using the template and logo that can be customised using +`OPENWISP_EMAIL_TEMPLATE <#openwisp_email_template>`_ and +`OPENWISP_EMAIL_LOGO <#openwisp_email_logo>`_ respectively). -In case the HTML version if not needed it may be disabled by -setting `OPENWISP_HTML_EMAIL <#openwisp_html_email>`_ to ``False``. +In case the HTML version if not needed it may be disabled by setting +`OPENWISP_HTML_EMAIL <#openwisp_html_email>`_ to ``False``. **Syntax:** @@ -1437,43 +1495,37 @@ setting `OPENWISP_HTML_EMAIL <#openwisp_html_email>`_ to ``False``. send_email(subject, body_text, body_html, recipients, **kwargs) -+--------------------+--------------------------------------------------------------------------------------------+ -| **Parameter** | **Description** | -+--------------------+--------------------------------------------------------------------------------------------+ -| ``subject`` | (``str``) The subject of the email template. | -+--------------------+--------------------------------------------------------------------------------------------+ -| ``body_text`` | (``str``) The body of the text message to be emailed. | -+--------------------+--------------------------------------------------------------------------------------------+ -| ``body_html`` | (``str``) The body of the html template to be emailed. | -+--------------------+--------------------------------------------------------------------------------------------+ -| ``recipients`` | (``list``) The list of recipients to send the mail to. | -+--------------------+--------------------------------------------------------------------------------------------+ -| ``extra_context`` | **optional** (``dict``) Extra context which is passed to the template. | -| | The dictionary keys ``call_to_action_text`` and ``call_to_action_url`` | -| | can be passed to show a call to action button. | -| | Similarly, ``footer`` can be passed to add a footer. | -+--------------------+--------------------------------------------------------------------------------------------+ -| ``**kwargs`` | Any additional keyword arguments (e.g. ``attachments``, ``headers``, etc.) | -| | are passed directly to the `django.core.mail.EmailMultiAlternatives | -| | `_. | -+--------------------+--------------------------------------------------------------------------------------------+ - - -**Note**: Data passed in body should be validated and user supplied data should not be sent directly to the function. +================= ========================================================================================== +**Parameter** **Description** +``subject`` (``str``) The subject of the email template. +``body_text`` (``str``) The body of the text message to be emailed. +``body_html`` (``str``) The body of the html template to be emailed. +``recipients`` (``list``) The list of recipients to send the mail to. +``extra_context`` **optional** (``dict``) Extra context which is passed to the template. The dictionary keys + ``call_to_action_text`` and ``call_to_action_url`` can be passed to show a call to action + button. Similarly, ``footer`` can be passed to add a footer. +``**kwargs`` Any additional keyword arguments (e.g. ``attachments``, ``headers``, etc.) are passed + directly to the `django.core.mail.EmailMultiAlternatives + `_. +================= ========================================================================================== + +**Note**: Data passed in body should be validated and user supplied data +should not be sent directly to the function. REST API utilities ------------------ ``openwisp_utils.api.serializers.ValidatedModelSerializer`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A model serializer which calls the model instance ``full_clean()``. ``openwisp_utils.api.apps.ApiAppConfig`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're creating an OpenWISP module which provides a REST API built with Django REST Framework, -chances is that you may need to define some default settings to control its throttling or other aspects. +If you're creating an OpenWISP module which provides a REST API built with +Django REST Framework, chances is that you may need to define some default +settings to control its throttling or other aspects. Here's how to easily do it: @@ -1485,26 +1537,28 @@ Here's how to easily do it: class MyModuleConfig(ApiAppConfig): - name = 'my_openwisp_module' - label = 'my_module' - verbose_name = _('My OpenWISP Module') + name = "my_openwisp_module" + label = "my_module" + verbose_name = _("My OpenWISP Module") # assumes API is enabled by default - API_ENABLED = getattr(settings, 'MY_OPENWISP_MODULE_API_ENABLED', True) + API_ENABLED = getattr( + settings, "MY_OPENWISP_MODULE_API_ENABLED", True + ) # set throttling rates for your module here REST_FRAMEWORK_SETTINGS = { - 'DEFAULT_THROTTLE_RATES': {'my_module': '400/hour'}, + "DEFAULT_THROTTLE_RATES": {"my_module": "400/hour"}, } Every openwisp module which has an API should use this class to configure -its own default settings, which will be merged with the settings of the other -modules. +its own default settings, which will be merged with the settings of the +other modules. Test utilities -------------- ``openwisp_utils.tests.catch_signal`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This method can be used to mock a signal call inorder to easily verify that the signal has been called. @@ -1518,33 +1572,37 @@ Usage example as a context-manager: with catch_signal(openwisp_signal) as handler: model_instance.trigger_signal() handler.assert_called_once_with( - arg1='value1', - arg2='value2', + arg1="value1", + arg2="value2", sender=ModelName, signal=openwisp_signal, ) ``openwisp_utils.tests.TimeLoggingTestRunner`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. figure:: https://raw.githubusercontent.com/openwisp/openwisp-utils/master/docs/TimeLoggingTestRunner.png - :align: center + :align: center -This class extends the `default test runner provided by Django `_ -and logs the time spent by each test, making it easier to spot slow tests by highlighting -time taken by it in yellow (time shall be highlighted in red if it crosses the second threshold). +This class extends the `default test runner provided by Django +`_ +and logs the time spent by each test, making it easier to spot slow tests +by highlighting time taken by it in yellow (time shall be highlighted in +red if it crosses the second threshold). -By default tests are considered slow if they take more than 0.3 seconds but you can control -this with `OPENWISP_SLOW_TEST_THRESHOLD <#openwisp_slow_test_threshold>`_. +By default tests are considered slow if they take more than 0.3 seconds +but you can control this with `OPENWISP_SLOW_TEST_THRESHOLD +<#openwisp_slow_test_threshold>`_. -In order to switch to this test runner you have set the following in your `settings.py`: +In order to switch to this test runner you have set the following in your +`settings.py`: .. code-block:: python - TEST_RUNNER = 'openwisp_utils.tests.TimeLoggingTestRunner' + TEST_RUNNER = "openwisp_utils.tests.TimeLoggingTestRunner" ``openwisp_utils.tests.capture_stdout`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This decorator can be used to capture standard output produced by tests, either to silence it or to write assertions. @@ -1555,32 +1613,36 @@ Example usage: from openwisp_utils.tests import capture_stdout + @capture_stdout() def test_something(self): - function_generating_output() # pseudo code + function_generating_output() # pseudo code + @capture_stdout() def test_something_again(self, captured_ouput): # pseudo code function_generating_output() # now you can create assertions on the captured output - self.assertIn('expected stdout', captured_ouput.getvalue()) + self.assertIn("expected stdout", captured_ouput.getvalue()) # if there are more than one assertions, clear the captured output first captured_error.truncate(0) captured_error.seek(0) # you can create new assertion now - self.assertIn('another output', captured_ouput.getvalue()) + self.assertIn("another output", captured_ouput.getvalue()) **Notes**: -- If assertions need to be made on the captured output, an additional argument - (in the example above is named ``captured_output``) can be passed as an argument - to the decorated test method, alternatively it can be omitted. -- A ``StingIO`` instance is used for capturing output by default but if needed - it's possible to pass a custom ``StringIO`` instance to the decorator function. +- If assertions need to be made on the captured output, an additional + argument (in the example above is named ``captured_output``) can be + passed as an argument to the decorated test method, alternatively it can + be omitted. +- A ``StingIO`` instance is used for capturing output by default but if + needed it's possible to pass a custom ``StringIO`` instance to the + decorator function. ``openwisp_utils.tests.capture_stderr`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Equivalent to ``capture_stdout``, but for standard error. @@ -1590,27 +1652,29 @@ Example usage: from openwisp_utils.tests import capture_stderr + @capture_stderr() def test_error(self): - function_generating_error() # pseudo code + function_generating_error() # pseudo code + @capture_stderr() def test_error_again(self, captured_error): # pseudo code function_generating_error() # now you can create assertions on captured error - self.assertIn('expected error', captured_error.getvalue()) + self.assertIn("expected error", captured_error.getvalue()) # if there are more than one assertions, clear the captured error first captured_error.truncate(0) captured_error.seek(0) # you can create new assertion now - self.assertIn('another expected error', captured_error.getvalue()) + self.assertIn("another expected error", captured_error.getvalue()) ``openwisp_utils.tests.capture_any_output`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Equivalent to ``capture_stdout`` and ``capture_stderr``, but captures both types of -output (standard output and standard error). +Equivalent to ``capture_stdout`` and ``capture_stderr``, but captures both +types of output (standard output and standard error). Example usage: @@ -1618,23 +1682,25 @@ Example usage: from openwisp_utils.tests import capture_any_output + @capture_any_output() def test_something_out(self): - function_generating_output() # pseudo code + function_generating_output() # pseudo code + @capture_any_output() def test_out_again(self, captured_output, captured_error): # pseudo code function_generating_output_and_errors() # now you can create assertions on captured error - self.assertIn('expected stdout', captured_output.getvalue()) - self.assertIn('expected stderr', captured_error.getvalue()) + self.assertIn("expected stdout", captured_output.getvalue()) + self.assertIn("expected stderr", captured_error.getvalue()) ``openwisp_utils.tests.AssertNumQueriesSubTestMixin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This mixin overrides the -`assertNumQueries `_ +This mixin overrides the `assertNumQueries +`_ assertion from the django test case to run in a ``subTest`` so that the query check does not block the whole test if it fails. @@ -1652,39 +1718,38 @@ Example usage: MyModel.objects.count() # the assertion above will fail but this line will be executed - print('This will be printed anyway.') + print("This will be printed anyway.") ``openwisp_utils.test_selenium_mixins.SeleniumTestMixin`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This mixin provides basic setup for Selenium tests with method to -open URL and login and logout a user. +This mixin provides basic setup for Selenium tests with method to open URL +and login and logout a user. Database backends ----------------- ``openwisp_utils.db.backends.spatialite`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This backend extends ``django.contrib.gis.db.backends.spatialite`` -database backend to implement a workaround for handling -`issue with sqlite 3.36 and spatialite 5 `_. +database backend to implement a workaround for handling `issue with sqlite +3.36 and spatialite 5 `_. Collection of Usage Metrics --------------------------- -The ``openwisp-utils`` module includes an optional -sub-app ``openwisp_utils.metric_collection``, -which allows us to collect of the following information -from OpenWISP instances: +The ``openwisp-utils`` module includes an optional sub-app +``openwisp_utils.metric_collection``, which allows us to collect of the +following information from OpenWISP instances: - OpenWISP Version - List of enabled OpenWISP modules and their version -- Operating System identifier, e.g.: - Linux version, Kernel version, target platform (e.g. x86) +- Operating System identifier, e.g.: Linux version, Kernel version, target + platform (e.g. x86) - Installation method, if available, e.g. `ansible-openwisp2 - `_ - or `docker-openwisp `_ + `_ or `docker-openwisp + `_ The data above is collected during the following events: @@ -1697,84 +1762,97 @@ and upgrade patterns. This informs our development decisions, ensuring continuous improvement aligned with user needs. To enhance our understanding and management of this data, we have -integrated `Clean Insights `_, a privacy-preserving -analytics tool. Clean Insights allows us to responsibly gather and analyze -usage metrics without compromising user privacy. It provides us with the -means to make data-driven decisions while respecting our users' rights and trust. +integrated `Clean Insights `_, a +privacy-preserving analytics tool. Clean Insights allows us to responsibly +gather and analyze usage metrics without compromising user privacy. It +provides us with the means to make data-driven decisions while respecting +our users' rights and trust. -We have taken great care to ensure no -sensitive or personal data is being tracked. +We have taken great care to ensure no sensitive or personal data is being +tracked. Opting out from metric collection -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can opt-out from sharing this data any time from the "System Info" page. -Alternatively, you can also remove the ``openwisp_utils.metric_collection`` -app from ``INSTALLED_APPS`` in one of the following ways: +You can opt-out from sharing this data any time from the "System Info" +page. Alternatively, you can also remove the +``openwisp_utils.metric_collection`` app from ``INSTALLED_APPS`` in one of +the following ways: - If you are using the `ansible-openwisp2 `_ role, you can set the - variable ``openwisp2_usage_metric_collection`` to ``false`` in your playbook. - + variable ``openwisp2_usage_metric_collection`` to ``false`` in your + playbook. - If you are using `docker-openwisp `_, you can set set the - environment variable ``METRIC_COLLECTION`` to ``False`` in the ``.env`` file. + environment variable ``METRIC_COLLECTION`` to ``False`` in the ``.env`` + file. -However, it would be very helpful to the project if you keep the -colection of these metrics enabled, because the feedback we get from -this data is useful to guide the project in the right direction. +However, it would be very helpful to the project if you keep the colection +of these metrics enabled, because the feedback we get from this data is +useful to guide the project in the right direction. Quality Assurance Checks ------------------------ -This package contains some common QA checks that are used in the -automated builds of different OpenWISP modules. +This package contains some common QA checks that are used in the automated +builds of different OpenWISP modules. ``openwisp-qa-format`` -^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~ -This shell script automatically formats Python and CSS code according -to the `OpenWISP coding style conventions `_. +This shell script automatically formats Python and CSS code according to +the `OpenWISP coding style conventions +`_. -It runs ``isort`` and ``black`` to format python code -(these two dependencies are required and installed automatically when running -``pip install openwisp-utils[qa]``). +It runs ``isort`` and ``black`` to format python code (these two +dependencies are required and installed automatically when running ``pip +install openwisp-utils[qa]``). -The ``stylelint`` and ``jshint`` programs are used to perform style checks on CSS and JS code respectively, but they are optional: -if ``stylelint`` and/or ``jshint`` are not installed, the check(s) will be skipped. +The ``stylelint`` and ``jshint`` programs are used to perform style checks +on CSS and JS code respectively, but they are optional: if ``stylelint`` +and/or ``jshint`` are not installed, the check(s) will be skipped. ``openwisp-qa-check`` -^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~ Shell script to run the following quality assurance checks: -* `checkmigrations <#checkmigrations>`_ -* `checkcommit <#checkcommit>`_ -* `checkendline <#checkendline>`_ -* `checkpendingmigrations <#checkpendingmigrations>`_ -* `checkrst <#checkrst>`_ -* ``flake8`` - Python code linter -* ``isort`` - Sorts python imports alphabetically, and seperated into sections -* ``black`` - Formats python code using a common standard -* ``csslinter`` - Formats and checks CSS code using stylelint common standard -* ``jslinter`` - Checks Javascript code using jshint common standard +- `checkmigrations <#checkmigrations>`_ +- `checkcommit <#checkcommit>`_ +- `checkendline <#checkendline>`_ +- `checkpendingmigrations <#checkpendingmigrations>`_ +- `checkrst <#checkrst>`_ +- ``flake8`` - Python code linter +- ``isort`` - Sorts python imports alphabetically, and seperated into + sections +- ``black`` - Formats python code using a common standard +- ``csslinter`` - Formats and checks CSS code using stylelint common + standard +- ``jslinter`` - Checks Javascript code using jshint common standard If a check requires a flag, it can be passed forward in the same way. -Usage example:: +Usage example: + +:: openwisp-qa-check --migration-path --message Any unneeded checks can be skipped by passing ``--skip-`` -Usage example:: +Usage example: + +:: openwisp-qa-check --skip-isort -For backward compatibility ``csslinter`` and ``jslinter`` are skipped by default. -To run them in checks pass arguements in this way. +For backward compatibility ``csslinter`` and ``jslinter`` are skipped by +default. To run them in checks pass arguements in this way. + +Usage example: -Usage example:: +:: # To activate csslinter openwisp-qa-check --csslinter @@ -1782,9 +1860,12 @@ Usage example:: # To activate jslinter openwisp-qa-check --jslinter -You can do multiple ``checkmigrations`` by passing the arguments with space-delimited string. +You can do multiple ``checkmigrations`` by passing the arguments with +space-delimited string. -For example, this multiple ``checkmigrations``:: +For example, this multiple ``checkmigrations``: + +:: checkmigrations --migrations-to-ignore 3 \ --migration-path ./openwisp_users/migrations/ || exit 1 @@ -1792,43 +1873,53 @@ For example, this multiple ``checkmigrations``:: checkmigrations --migrations-to-ignore 2 \ --migration-path ./tests/testapp/migrations/ || exit 1 -Can be changed with:: +Can be changed with: + +:: openwisp-qa-check --migrations-to-ignore "3 2" \ --migration-path "./openwisp_users/migrations/ ./tests/testapp/migrations/" ``checkmigrations`` -^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~ Ensures the latest migrations created have a human readable name. -We want to avoid having many migrations named like ``0003_auto_20150410_3242.py``. +We want to avoid having many migrations named like +``0003_auto_20150410_3242.py``. -This way we can reconstruct the evolution of our database schemas faster, with -less efforts and hence less costs. +This way we can reconstruct the evolution of our database schemas faster, +with less efforts and hence less costs. -Usage example:: +Usage example: + +:: checkmigrations --migration-path ./django_freeradius/migrations/ ``checkcommit`` -^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~ -Ensures the last commit message follows our `commit message style guidelines +Ensures the last commit message follows our `commit message style +guidelines `_. -We want to keep the commit log readable, consistent and easy to scan in order -to make it easy to analyze the history of our modules, which is also a very -important activity when performing maintenance. +We want to keep the commit log readable, consistent and easy to scan in +order to make it easy to analyze the history of our modules, which is also +a very important activity when performing maintenance. + +Usage example: -Usage example:: +:: checkcommit --message "$(git log --format=%B -n 1)" -If, for some reason, you wish to skip this QA check for a specific commit message -you can add ``#noqa`` to the end of your commit message. +If, for some reason, you wish to skip this QA check for a specific commit +message you can add ``#noqa`` to the end of your commit message. -Usage example:: +Usage example: + +:: [qa] Improved #20 @@ -1836,109 +1927,128 @@ Usage example:: #noqa ``checkendline`` -^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~ Ensures that a blank line is kept at the end of each file. ``checkpendingmigrations`` -^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~ -Ensures there django migrations are up to date and no new migrations need to -be created. +Ensures there django migrations are up to date and no new migrations need +to be created. -It accepts an optional ``--migration-module`` flag indicating the django app -name that should be passed to ``./manage.py makemigrations``, eg: +It accepts an optional ``--migration-module`` flag indicating the django +app name that should be passed to ``./manage.py makemigrations``, eg: ``./manage.py makemigrations $MIGRATION_MODULE``. ``checkrst`` -^^^^^^^^^^^^^ +~~~~~~~~~~~~ -Checks the syntax of all ReStructuredText files to ensure they can be published on pypi or using python-sphinx. +Checks the syntax of all ReStructuredText files to ensure they can be +published on pypi or using python-sphinx. Settings -------- ``OPENWISP_ADMIN_SITE_CLASS`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``openwisp_utils.admin_theme.admin.OpenwispAdminSite`` -If you need to use a customized admin site class, you can use this setting. +If you need to use a customized admin site class, you can use this +setting. ``OPENWISP_ADMIN_SITE_TITLE`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``OpenWISP Admin`` Title value used in the ```` HTML tag of the admin site. ``OPENWISP_ADMIN_SITE_HEADER`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``OpenWISP`` -Heading text used in the main ``<h1>`` HTML tag (the logo) of the admin site. +Heading text used in the main ``<h1>`` HTML tag (the logo) of the admin +site. ``OPENWISP_ADMIN_INDEX_TITLE`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``Network administration`` Title shown to users in the index page of the admin site. ``OPENWISP_ADMIN_DASHBOARD_ENABLED`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``True`` When ``True``, enables the `OpenWISP Dashboard <#openwisp-dashboard>`_. -Upon login, the user will be greeted with the dashboard instead of the default -Django admin index page. +Upon login, the user will be greeted with the dashboard instead of the +default Django admin index page. ``OPENWISP_ADMIN_THEME_LINKS`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``[]`` -**Note**: this setting requires -`the admin_theme_settings context processor <#supplying-custom-css-and-js-for-the-admin-theme>`_ -in order to work. +**Note**: this setting requires `the admin_theme_settings context +processor <#supplying-custom-css-and-js-for-the-admin-theme>`_ in order to +work. Allows to override the default CSS and favicon, as well as add extra <link> HTML elements if needed. -This setting overrides the default theme, you can reuse the default CSS or replace it entirely. +This setting overrides the default theme, you can reuse the default CSS or +replace it entirely. -The following example shows how to keep using the default CSS, -supply an additional CSS and replace the favicon. +The following example shows how to keep using the default CSS, supply an +additional CSS and replace the favicon. Example usage: .. code-block:: python OPENWISP_ADMIN_THEME_LINKS = [ - {'type': 'text/css', 'href': '/static/admin/css/openwisp.css', 'rel': 'stylesheet', 'media': 'all'}, - {'type': 'text/css', 'href': '/static/admin/css/custom-theme.css', 'rel': 'stylesheet', 'media': 'all'}, - {'type': 'image/x-icon', 'href': '/static/favicon.png', 'rel': 'icon'} + { + "type": "text/css", + "href": "/static/admin/css/openwisp.css", + "rel": "stylesheet", + "media": "all", + }, + { + "type": "text/css", + "href": "/static/admin/css/custom-theme.css", + "rel": "stylesheet", + "media": "all", + }, + { + "type": "image/x-icon", + "href": "/static/favicon.png", + "rel": "icon", + }, ] ``OPENWISP_ADMIN_THEME_JS`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``[]`` -Allows to pass a list of strings representing URLs of custom JS files to load. +Allows to pass a list of strings representing URLs of custom JS files to +load. Example usage: .. code-block:: python OPENWISP_ADMIN_THEME_JS = [ - '/static/custom-admin-theme.js', + "/static/custom-admin-theme.js", ] ``OPENWISP_ADMIN_SHOW_USERLINKS_BLOCK`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``False`` @@ -1946,94 +2056,100 @@ When True, enables Django user links on the admin site. i.e. (USER NAME/ VIEW SITE / CHANGE PASSWORD / LOG OUT). -These links are already shown in the main navigation menu and for this reason are hidden by default. +These links are already shown in the main navigation menu and for this +reason are hidden by default. ``OPENWISP_API_DOCS`` -^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~ **default**: ``True`` Whether the OpenAPI documentation is enabled. -When enabled, you can view the available documentation using the -Swagger endpoint at ``/api/v1/docs/``. +When enabled, you can view the available documentation using the Swagger +endpoint at ``/api/v1/docs/``. You also need to add the following url to your project urls.py: .. code-block:: python urlpatterns += [ - url(r'^api/v1/', include('openwisp_utils.api.urls')), + url(r"^api/v1/", include("openwisp_utils.api.urls")), ] ``OPENWISP_API_INFO`` -^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~ **default**: .. code-block:: python { - 'title': 'OpenWISP API', - 'default_version': 'v1', - 'description': 'OpenWISP REST API', + "title": "OpenWISP API", + "default_version": "v1", + "description": "OpenWISP REST API", } -Define OpenAPI general information. -NOTE: This setting requires ``OPENWISP_API_DOCS = True`` to take effect. +Define OpenAPI general information. NOTE: This setting requires +``OPENWISP_API_DOCS = True`` to take effect. -For more information about optional parameters check the -`drf-yasg documentation <https://drf-yasg.readthedocs.io/en/stable/readme.html#quickstart>`_. +For more information about optional parameters check the `drf-yasg +documentation +<https://drf-yasg.readthedocs.io/en/stable/readme.html#quickstart>`_. ``OPENWISP_SLOW_TEST_THRESHOLD`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``[0.3, 1]`` (seconds) -It can be used to change the thresholds used by `TimeLoggingTestRunner <#openwisp_utilsteststimeloggingtestrunner>`_ -to detect slow tests (0.3s by default) and highlight the slowest ones (1s by default) amongst them. +It can be used to change the thresholds used by `TimeLoggingTestRunner +<#openwisp_utilsteststimeloggingtestrunner>`_ to detect slow tests (0.3s +by default) and highlight the slowest ones (1s by default) amongst them. ``OPENWISP_STATICFILES_VERSIONED_EXCLUDE`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **default**: ``['leaflet/*/*.png']`` -Allows to pass a list of **Unix shell-style wildcards** for files to be excluded by `CompressStaticFilesStorage <#openwisp_utilsstorageCompressStaticFilesStorage>`_. +Allows to pass a list of **Unix shell-style wildcards** for files to be +excluded by `CompressStaticFilesStorage +<#openwisp_utilsstorageCompressStaticFilesStorage>`_. -By default Leaflet PNGs have been excluded to avoid bugs like `openwisp/ansible-openwisp2#232 <https://github.com/openwisp/ansible-openwisp2/issues/232>`_. +By default Leaflet PNGs have been excluded to avoid bugs like +`openwisp/ansible-openwisp2#232 +<https://github.com/openwisp/ansible-openwisp2/issues/232>`_. Example usage: .. code-block:: python OPENWISP_STATICFILES_VERSIONED_EXCLUDE = [ - '*png', + "*png", ] ``OPENWISP_HTML_EMAIL`` -^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~ -+---------+----------+ -| type | ``bool`` | -+---------+----------+ -| default | ``True`` | -+---------+----------+ +======= ======== +type ``bool`` +default ``True`` +======= ======== -If ``True``, an HTML themed version of the email can be sent using -the `send_email <#openwisp_utilsadmin_themeemailsend_email>`_ function. +If ``True``, an HTML themed version of the email can be sent using the +`send_email <#openwisp_utilsadmin_themeemailsend_email>`_ function. ``OPENWISP_EMAIL_TEMPLATE`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+---------+----------------------------------------+ -| type | ``str`` | -+---------+----------------------------------------+ -| default | ``openwisp_utils/email_template.html`` | -+---------+----------------------------------------+ +======= ====================================== +type ``str`` +default ``openwisp_utils/email_template.html`` +======= ====================================== -This setting allows to change the django template used for sending emails with -the `send_email <#openwisp_utilsadmin_themeemailsend_email>`_ function. -It is recommended to extend the default email template as in the example below. +This setting allows to change the django template used for sending emails +with the `send_email <#openwisp_utilsadmin_themeemailsend_email>`_ +function. It is recommended to extend the default email template as in the +example below. .. code-block:: django @@ -2056,59 +2172,58 @@ It is recommended to extend the default email template as in the example below. </style> {% endblock styles %} -Similarly, you can customize the HTML of the template by overriding the ``body`` block. -See `email_template.html <https://github.com/openwisp/openwisp-utils/blob/ -master/openwisp_utils/admin_theme/templates/openwisp_utils/email_template.html>`_ +Similarly, you can customize the HTML of the template by overriding the +``body`` block. See `email_template.html +<https://github.com/openwisp/openwisp-utils/blob/master/openwisp_utils/admin_theme/templates/openwisp_utils/email_template.html>`_ for reference implementation. ``OPENWISP_EMAIL_LOGO`` -^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~ -+---------+-------------------------------------------------------------------------------------+ -| type | ``str`` | -+---------+-------------------------------------------------------------------------------------+ -| default | `OpenWISP logo <https://raw.githubusercontent.com/openwisp/openwisp-utils/master/ \ | -| | openwisp_utils/static/openwisp-utils/images/openwisp-logo.png>`_ | -+---------+-------------------------------------------------------------------------------------+ +======= ================================================================================================================================== +type ``str`` +default `OpenWISP logo + <https://raw.githubusercontent.com/openwisp/openwisp-utils/master/openwisp_utils/static/openwisp-utils/images/openwisp-logo.png>`_ +======= ================================================================================================================================== -This setting allows to change the logo which is displayed in HTML version of the email. +This setting allows to change the logo which is displayed in HTML version +of the email. -**Note**: Provide a URL which points to the logo on your own web server. Ensure that the URL provided is -publicly accessible from the internet. Otherwise, the logo may not be displayed in the email. -Please also note that SVG images do not get processed by some email clients -like Gmail so it is recommended to use PNG images. +**Note**: Provide a URL which points to the logo on your own web server. +Ensure that the URL provided is publicly accessible from the internet. +Otherwise, the logo may not be displayed in the email. Please also note +that SVG images do not get processed by some email clients like Gmail so +it is recommended to use PNG images. ``OPENWISP_CELERY_SOFT_TIME_LIMIT`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+---------+---------------------+ -| type | ``int`` | -+---------+---------------------+ -| default | ``30`` (in seconds) | -+---------+---------------------+ +======= =================== +type ``int`` +default ``30`` (in seconds) +======= =================== -Sets the soft time limit for celery tasks using -`OpenwispCeleryTask <#openwisp_utilstasksopenwispcelerytask>`_. +Sets the soft time limit for celery tasks using `OpenwispCeleryTask +<#openwisp_utilstasksopenwispcelerytask>`_. ``OPENWISP_CELERY_HARD_TIME_LIMIT`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+---------+----------------------+ -| type | ``int`` | -+---------+----------------------+ -| default | ``120`` (in seconds) | -+---------+----------------------+ +======= ==================== +type ``int`` +default ``120`` (in seconds) +======= ==================== -Sets the hard time limit for celery tasks using -`OpenwispCeleryTask <#openwisp_utilstasksopenwispcelerytask>`_. +Sets the hard time limit for celery tasks using `OpenwispCeleryTask +<#openwisp_utilstasksopenwispcelerytask>`_. ``OPENWISP_AUTOCOMPLETE_FILTER_VIEW`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -+---------+-------------------------------------------------------------+ -| type | ``str`` | -+---------+-------------------------------------------------------------+ -| default | ``'openwisp_utils.admin_theme.views.AutocompleteJsonView'`` | -+---------+-------------------------------------------------------------+ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +======= =========================================================== +type ``str`` +default ``'openwisp_utils.admin_theme.views.AutocompleteJsonView'`` +======= =========================================================== Dotted path to the ``AutocompleteJsonView`` used by the ``openwisp_utils.admin_theme.filters.AutocompleteFilter``. @@ -2145,14 +2260,17 @@ Install node dependencies used for testing: npm install -g stylelint jshint -Set up the pre-push hook to run tests and QA checks automatically right before the git push action, so that if anything fails the push operation will be aborted: +Set up the pre-push hook to run tests and QA checks automatically right +before the git push action, so that if anything fails the push operation +will be aborted: .. code-block:: shell openwisp-pre-push-hook --install -Install WebDriver for Chromium for your browser version from `<https://chromedriver.chromium.org/home>`_ -and Extract ``chromedriver`` to one of directories from your ``$PATH`` (example: ``~/.local/bin/``). +Install WebDriver for Chromium for your browser version from +https://chromedriver.chromium.org/home and Extract ``chromedriver`` to one +of directories from your ``$PATH`` (example: ``~/.local/bin/``). Create database: @@ -2169,7 +2287,8 @@ Run development server: cd tests/ ./manage.py runserver -You can access the admin interface of the test project at http://127.0.0.1:8000/admin/. +You can access the admin interface of the test project at +http://127.0.0.1:8000/admin/. Run tests with: @@ -2180,7 +2299,8 @@ Run tests with: Contributing ------------ -Please refer to the `OpenWISP contributing guidelines <http://openwisp.io/docs/developer/contributing.html>`_. +Please refer to the `OpenWISP contributing guidelines +<http://openwisp.io/docs/developer/contributing.html>`_. Support ------- @@ -2190,19 +2310,24 @@ See `OpenWISP Support Channels <http://openwisp.org/support.html>`_. Changelog --------- -See `CHANGES <https://github.com/openwisp/openwisp-utils/blob/master/CHANGES.rst>`_. +See `CHANGES +<https://github.com/openwisp/openwisp-utils/blob/master/CHANGES.rst>`_. License ------- -See `LICENSE <https://github.com/openwisp/openwisp-utils/blob/master/LICENSE>`_. +See `LICENSE +<https://github.com/openwisp/openwisp-utils/blob/master/LICENSE>`_. Attribution ----------- -- `Wireless icon <https://github.com/openwisp/openwisp-utils/blob/master/openwisp_utils/admin_theme/static/ui/openwisp/images/monitoring-wifi.svg>`_ - is licensed by Gregbaker, under `CC BY-SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0>`_ , - via `Wikimedia Commons <https://commons.wikimedia.org/wiki/File:Wireless-icon.svg>`_. -- `Roboto webfont <https://www.google.com/fonts/specimen/Roboto>`_ is licensed - under the `Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>`_. - WOFF files extracted using `<https://github.com/majodev/google-webfonts-helper>`_. +- `Wireless icon + <https://github.com/openwisp/openwisp-utils/blob/master/openwisp_utils/admin_theme/static/ui/openwisp/images/monitoring-wifi.svg>`_ + is licensed by Gregbaker, under `CC BY-SA 4.0 + <https://creativecommons.org/licenses/by-sa/4.0>`_ , via `Wikimedia + Commons <https://commons.wikimedia.org/wiki/File:Wireless-icon.svg>`_. +- `Roboto webfont <https://www.google.com/fonts/specimen/Roboto>`_ is + licensed under the `Apache License, Version 2.0 + <https://www.apache.org/licenses/LICENSE-2.0>`_. WOFF files extracted + using https://github.com/majodev/google-webfonts-helper. diff --git a/openwisp-qa-check b/openwisp-qa-check index 93fd3d8b..890bf40c 100755 --- a/openwisp-qa-check +++ b/openwisp-qa-check @@ -125,8 +125,14 @@ runflake8() { } runrstcheck() { - checkrst && echo "SUCCESS: ReStructuredText check successful!" \ - || { echoerr "ERROR: ReStructuredText check failed!"; FAILURE=1; } + cmd="docstrfmt --no-docstring-trailing-line --ignore-cache --check --line-length 74 ." + $($cmd --quiet) && + echo "SUCCESS: ReStructuredText check successful!" || + { + echoerr "ERROR: ReStructuredText check failed!" + $($cmd) + FAILURE=1 + } } runisort() { diff --git a/openwisp-qa-format b/openwisp-qa-format index 2037b7a5..66920524 100755 --- a/openwisp-qa-format +++ b/openwisp-qa-format @@ -3,6 +3,7 @@ set -e isort . black -S . +docstrfmt --no-docstring-trailing-line --ignore-cache --line-length 74 . if which stylelint > /dev/null; then stylelint $(find . -type f -name "*.css") --fix diff --git a/openwisp_utils/admin.py b/openwisp_utils/admin.py index a7339f10..a83319fa 100644 --- a/openwisp_utils/admin.py +++ b/openwisp_utils/admin.py @@ -5,10 +5,7 @@ class TimeReadonlyAdminMixin(object): - """ - mixin that automatically flags - `created` and `modified` as readonly - """ + """A mixin that automatically flags `created` and `modified` as readonly.""" def __init__(self, *args, **kwargs): self.readonly_fields += ('created', 'modified') @@ -16,9 +13,7 @@ def __init__(self, *args, **kwargs): class ReadOnlyAdmin(ModelAdmin): - """ - Disables all editing capabilities - """ + """Disables all editing capabilities.""" exclude = tuple() @@ -59,11 +54,11 @@ def change_view(self, request, object_id, extra_context=None): class AlwaysHasChangedMixin(object): def has_changed(self): - """ - This django-admin trick ensures the inline item - is saved even if default values are unchanged - (without this trick new objects won't be - created unless users change the default values) + """Returns true for new objects. + + This django-admin trick ensures the inline item is saved even if + default values are unchanged (without this trick new objects won't + be created unless users change the default values). """ if self.instance._state.adding: return True @@ -75,12 +70,10 @@ class CopyableFieldError(FieldError): class CopyableFieldsAdmin(ModelAdmin): - """ - An admin class that allows to set admin - fields to be read-only and makes it easy - to copy the fields contents. - Useful for auto-generated fields such as - UUIDs, secret keys, tokens, etc + """Allows to set fields as read-only and easy to copy. + + Useful for auto-generated fields such as UUIDs, secret keys, tokens, + etc. """ copyable_fields = () @@ -142,12 +135,11 @@ class Media: class UUIDAdmin(CopyableFieldsAdmin): - """ - This class is a subclass of `CopyableFieldsAdmin` - which sets `uuid` as the only copyable field - This class is kept for backward compatibility - and convenience, since different models of various - OpenWISP modules show `uuid` as the only copyable field + """Sets `uuid` as copyable field. + + Subclass of `CopyableFieldsAdmin`. This class is kept for backward + compatibility and convenience, since different models of various + OpenWISP modules show `uuid` as the only copyable field. """ copyable_fields = ('uuid',) @@ -159,10 +151,13 @@ def uuid(self, obj): class ReceiveUrlAdmin(ModelAdmin): - """ - Return a receive_url field whose value is that of - a view_name concatenated with the obj id and/or - with the key of the obj + """Adds a receive_url field. + + The receive_url method will build the URL using the parameters: + + - receive_url_name + - receive_url_object_arg + - receive_url_object_arg """ receive_url_querystring_arg = 'key' @@ -180,9 +175,7 @@ def change_view(self, request, *args, **kwargs): return super().change_view(request, *args, **kwargs) def receive_url(self, obj): - """ - :param obj: Object for which the url is generated - """ + """:param obj: Object for which the url is generated""" if self.receive_url_name is None: raise ValueError('receive_url_name is not set up') reverse_kwargs = {} diff --git a/openwisp_utils/admin_theme/admin.py b/openwisp_utils/admin_theme/admin.py index 79364916..7aab2f99 100644 --- a/openwisp_utils/admin_theme/admin.py +++ b/openwisp_utils/admin_theme/admin.py @@ -64,15 +64,3 @@ def get_urls(self): name='ow-info', ), ] + super().get_urls() - - -def openwisp_admin(site_url=None): # pragma: no cover - """ - openwisp_admin function is deprecated - """ - logger.warning( - 'WARNING! Calling openwisp_utils.admin_theme.admin.openwisp_admin() ' - 'is not necessary anymore and is therefore deprecated.\nThis function ' - 'will be removed in future versions of openwisp-utils and therefore ' - 'it is recommended to remove any reference to it.\n' - ) diff --git a/openwisp_utils/admin_theme/apps.py b/openwisp_utils/admin_theme/apps.py index 261aed6f..07f02cb1 100644 --- a/openwisp_utils/admin_theme/apps.py +++ b/openwisp_utils/admin_theme/apps.py @@ -9,18 +9,22 @@ def _staticfy(value): - """ + """Backard compatible call to static(). + Allows to keep backward compatibility with instances of OpenWISP which were using the previous implementation of OPENWISP_ADMIN_THEME_LINKS and OPENWISP_ADMIN_THEME_JS which didn't automatically pre-process those lists of static files with django.templatetags.static.static() - and hence were not configured to allow those files to be found - by the staticfile loaders, if static() raises ValueError, we assume - one of either cases: - 1. An old instance has upgraded and we keep returning the old value - so the file will continue being found although unprocessed by - django's static file machinery. - 2. The value passed is wrong, instead of failing loudly we fail silently. + and hence were not configured to allow those files to be found by the + staticfile loaders, if static() raises ValueError, we assume one of + either cases: + + 1. An old instance has upgraded and we keep returning the old value so + the file will continue being found although unprocessed by django's + static file machinery. + + 2. The value passed is wrong, instead of failing loudly we fail + silently. """ try: return static(value) diff --git a/openwisp_utils/admin_theme/dashboard.py b/openwisp_utils/admin_theme/dashboard.py index 1f3ccf6f..8c4fb896 100644 --- a/openwisp_utils/admin_theme/dashboard.py +++ b/openwisp_utils/admin_theme/dashboard.py @@ -38,9 +38,10 @@ def _validate_chart_config(config): def register_dashboard_chart(position, config): - """ - Registers a dashboard chart - register_dashboard_chart(int, dict) + """Registers a dashboard chart: + + The position argument indicates the order. The config argument + indicates the chart configuration details. """ if not isinstance(position, int): raise ImproperlyConfigured('Dashboard chart position should be of type `int`.') @@ -57,10 +58,7 @@ def register_dashboard_chart(position, config): def unregister_dashboard_chart(name): - """ - Un-registers a dashboard chart - unregister_dashboard_chart(str) - """ + """Un-registers a dashboard chart.""" if not isinstance(name, str): raise ImproperlyConfigured('Dashboard chart name should be type `str`') @@ -82,10 +80,7 @@ def _validate_template_config(config): def register_dashboard_template( position, config, extra_config=None, after_charts=False ): - """ - Registers a dashboard template - register_dashboard_template(int, dict) - """ + """Registers a dashboard HTML template.""" if not isinstance(position, int): raise ImproperlyConfigured( 'Dashboard template position should be of type `int`.' @@ -112,10 +107,7 @@ def register_dashboard_template( def unregister_dashboard_template(path): - """ - Un-registers a dashboard template - unregister_dashboard_template(str) - """ + """Un-registers a dashboard template.""" if not isinstance(path, str): raise ImproperlyConfigured('Dashboard template path should be type `str`') @@ -130,9 +122,7 @@ def unregister_dashboard_template(path): def get_dashboard_context(request): - """ - Loads dashboard context for the admin index view - """ + """Loads dashboard context for the admin index view.""" context = {'is_popup': False, 'has_permission': True, 'dashboard_enabled': True} config = copy.deepcopy(DASHBOARD_CHARTS) diff --git a/openwisp_utils/admin_theme/filters.py b/openwisp_utils/admin_theme/filters.py index ed097b2a..e4fbfd86 100644 --- a/openwisp_utils/admin_theme/filters.py +++ b/openwisp_utils/admin_theme/filters.py @@ -28,10 +28,9 @@ def choices(self, changelist): yield all_choice def value(self): - """ - Return the value (in string format) provided in the request's - query string for this filter, if any, or None if the value wasn't - provided. + """Returns the querystring for this filter + + If no querystring was supllied, will return None. """ return self.used_parameters.get(self.parameter_name) diff --git a/openwisp_utils/admin_theme/menu.py b/openwisp_utils/admin_theme/menu.py index f869e409..2263a16f 100644 --- a/openwisp_utils/admin_theme/menu.py +++ b/openwisp_utils/admin_theme/menu.py @@ -9,9 +9,9 @@ class BaseMenuItem: - """ - It is a base class for all types of menu items. - It is used to handle some common functions. + """Base class for all menu items. + + Used to implement common functionality for menu items. """ def __init__(self, config): @@ -34,9 +34,9 @@ def set_label(self, config): class ModelLink(BaseMenuItem): - """ - It is to used create a link for a model, like "list view" and "add view". - Parameters for the config: name, model, label, icon + """Implements links to model objects. + + Input Parameters: name, model, label, icon. """ def __init__(self, config): @@ -88,9 +88,9 @@ def create_context(self, request): class MenuLink(BaseMenuItem): - """ - It is used to create any general link by supplying a custom url. - Parameters of config: label, url and icon. + """Generic Links. + + Creates a link from a custom url Input parameters: label, url, icon. """ def __init__(self, config): @@ -108,10 +108,11 @@ def __init__(self, config): class MenuGroup(BaseMenuItem): - """ - It is used to create a dropdown in the menu. - Parameters of config: label, items and icon. - each item in items should repesent a config for MenuLink or ModelLink + """Implements Menu Groups (dropdown). + + Input parameters: label, items and icon. The items is a dict in which + keys are positions and values must repesent a config for MenuLink or + ModelLink objects. """ def __init__(self, config): @@ -141,7 +142,8 @@ def set_items(self, items, config): if not isinstance(item, dict): raise ImproperlyConfigured( - f'Each value of "items" should be a type of "dict". Error for "items" of config- {config}' + f'Each value of "items" should be a type of "dict". ' + f'Error for "items" of config- {config}' ) if item.get('url'): # It is a menu link diff --git a/openwisp_utils/admin_theme/templatetags/ow_tags.py b/openwisp_utils/admin_theme/templatetags/ow_tags.py index 96a93919..dd9fb2b2 100644 --- a/openwisp_utils/admin_theme/templatetags/ow_tags.py +++ b/openwisp_utils/admin_theme/templatetags/ow_tags.py @@ -27,7 +27,5 @@ def ow_create_filter(cl, spec, total_filters): @register.filter @stringfilter def join_string(value): - """ - Can be used to join strings with "-" to make id or class - """ + """Can be used to join strings with "-" to make id or class.""" return value.lower().replace(' ', '-') diff --git a/openwisp_utils/base.py b/openwisp_utils/base.py index a949ab8b..f2576a16 100644 --- a/openwisp_utils/base.py +++ b/openwisp_utils/base.py @@ -16,10 +16,7 @@ class Meta: class TimeStampedEditableModel(UUIDModel): - """ - An abstract base class model that provides self-updating - ``created`` and ``modified`` fields. - """ + """An abstract base class model that provides self-updating ``created`` and ``modified`` fields.""" created = AutoCreatedField(_('created'), editable=True) modified = AutoLastModifiedField(_('modified'), editable=True) diff --git a/openwisp_utils/fields.py b/openwisp_utils/fields.py index 731224ad..fb3ee180 100644 --- a/openwisp_utils/fields.py +++ b/openwisp_utils/fields.py @@ -50,13 +50,12 @@ def deconstruct(self): class FallbackFromDbValueMixin: - """ - Returns the fallback value when the value of the field - is falsy (None or ''). + """Returns the fallback value when empty. - It does not set the field's value to "None" when the value - is equal to the fallback value. This allows overriding of - the value when a user knows that the default will get changed. + Returns the fallback value when the value of the field is falsy (None + or ''). It does not set the field's value to "None" when the value is + equal to the fallback value. This allows overriding of the value when + a user knows that the default will get changed. """ def from_db_value(self, value, expression, connection): @@ -66,10 +65,10 @@ def from_db_value(self, value, expression, connection): class FalsyValueNoneMixin: - """ - If the field contains an empty string, then - stores "None" in the database if the field is - nullable. + """Stores None instead of empty strings. + + If the field contains an empty string and the field can be NULL, this + mixin will prefer to store "None" in the database. """ # Django convention is to use the empty string, not NULL @@ -134,9 +133,10 @@ class FallbackPositiveIntegerField( class FallbackCharField( FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, CharField ): - """ - Populates the form with the fallback value - if the value is set to null in the database. + """Implements fallback logic. + + Populates the form with the fallback value if the value is set to NULL + in the database. """ pass @@ -145,9 +145,10 @@ class FallbackCharField( class FallbackURLField( FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, URLField ): - """ - Populates the form with the fallback value - if the value is set to null in the database. + """Implements fallback logic. + + Populates the form with the fallback value if the value is set to NULL + in the database. """ pass @@ -156,9 +157,10 @@ class FallbackURLField( class FallbackTextField( FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, TextField ): - """ - Populates the form with the fallback value - if the value is set to null in the database. + """Implements fallback logic. + + Populates the form with the fallback value if the value is set to NULL + in the database. """ def formfield(self, **kwargs): diff --git a/openwisp_utils/loaders.py b/openwisp_utils/loaders.py index 427d7b30..3389e846 100644 --- a/openwisp_utils/loaders.py +++ b/openwisp_utils/loaders.py @@ -7,9 +7,10 @@ class DependencyLoader(FilesystemLoader): - """ - A template loader that looks in templates dir of - django-apps listed in dependencies. Default values is [] + """Allows loading templates of apps listed in settings.EXTENDED_APPS. + + Looks in the "templates/"" directory of apps listed in + settings.EXTENDED_APPS. Defaults to []. """ dependencies = EXTENDED_APPS diff --git a/openwisp_utils/metric_collection/helper.py b/openwisp_utils/metric_collection/helper.py index 3db59545..11825b0c 100644 --- a/openwisp_utils/metric_collection/helper.py +++ b/openwisp_utils/metric_collection/helper.py @@ -6,11 +6,10 @@ class MetricCollectionAdminSiteHelper: - """ - Collection of helper methods designed - to be used in the admin_theme to show - the constent info message and allow - superusers to opt out. + """Collection of helper methods for the OpenWISP Admin Theme + + Designed to be used in the admin_theme to show the constent info + message and allow superusers to opt out. """ @classmethod @@ -25,13 +24,12 @@ def is_enabled_and_superuser(cls, user): @classmethod def show_consent_info(cls, request): - """ - Unless already shown, this method adds a - message (using the Django Message Framework) - to the request passed in as argument - to inform the super user about the OpenWISP - metric collection feature and the possibility - to opt out. + """Consent screen logic + + Unless already shown, this method adds a message (using the Django + Message Framework) to the request passed in as argument to inform + the super user about the OpenWISP metric collection feature and + the possibility to opt out. """ if not cls.is_enabled_and_superuser(request.user): return diff --git a/openwisp_utils/metric_collection/models.py b/openwisp_utils/metric_collection/models.py index 97dd9610..3833a622 100644 --- a/openwisp_utils/metric_collection/models.py +++ b/openwisp_utils/metric_collection/models.py @@ -29,10 +29,14 @@ class Meta: @classmethod def log_module_version_changes(cls, current_versions): - """ + """Logs changes to the version of installed OpenWISP modules. + Returns a tuple of booleans indicating: - - whether this is a new installation, - - whether any OpenWISP modules has been upgraded. + + - whether this is a new installation + - whether any OpenWISP modules has been upgraded. + + If no module has been upgraded, it won't store anything in the DB. """ openwisp_version = cls.objects.first() if not openwisp_version: @@ -139,10 +143,11 @@ def _post_metrics(cls, events): @classmethod def _get_events(cls, category, data): - """ - This method takes a category and data representing usage metrics, - and returns a list of events in a format accepted by the - Clean Insights Matomo Proxy (CIMP) API. + """Returns a list of events that will be sent to CleanInsights. + + This method requires two input parameters, category and data, + which represent usage metrics, and returns a list of events in a + format accepted by the Clean Insights Matomo Proxy (CIMP) API. Read the "Event Measurement Schema" in the CIMP documentation: https://cutt.ly/SwBkC40A @@ -170,12 +175,11 @@ def _get_events(cls, category, data): class Consent(TimeStampedEditableModel): - """ - This model stores information about the superuser's consent to collect - anonymous usage metrics. The ``shown_once`` field is used to - track whether the info message about the metric collection has been - shown to the superuser on their first login. - The ``user_consented`` field stores whether the superuser + """Stores consent to collect anonymous usage metrics. + + The ``shown_once`` field is used to track whether the info message + about the metric collection has been shown to the superuser on their + first login. The ``user_consented`` field stores whether the superuser has opted-out of collecting anonymous usage metrics. """ diff --git a/openwisp_utils/metric_collection/tests/runner.py b/openwisp_utils/metric_collection/tests/runner.py index 0fe69f1a..a5229bcc 100644 --- a/openwisp_utils/metric_collection/tests/runner.py +++ b/openwisp_utils/metric_collection/tests/runner.py @@ -9,10 +9,7 @@ class MockRequestPostRunner(TimeLoggingTestRunner): - """ - This runner ensures that usage metrics are - not sent in development when running tests. - """ + """This runner ensures that usage metrics are not sent in development when running tests.""" pass diff --git a/openwisp_utils/metric_collection/tests/test_models.py b/openwisp_utils/metric_collection/tests/test_models.py index 840891f5..859256b8 100644 --- a/openwisp_utils/metric_collection/tests/test_models.py +++ b/openwisp_utils/metric_collection/tests/test_models.py @@ -120,10 +120,8 @@ def test_install_detected_on_heartbeat_event(self, mocked_post, *args): @patch.object(OpenwispVersion, '_post_metrics') @freeze_time('2023-12-01 00:00:00') def test_install_not_detected_on_install_event(self, mocked_post, *args): - """ - Checks when the send_usage_metrics is triggered with "Install" category, - but there's no actual upgrade. - """ + # Checks when the send_usage_metrics is triggered + # with "Install" category, but there's no actual upgrade. self.assertEqual(OpenwispVersion.objects.count(), 1) tasks.send_usage_metrics(category='Install') mocked_post.assert_not_called() @@ -235,10 +233,7 @@ def test_upgrade_detected_on_heartbeat_event(self, mocked_post, *args): @patch.object(OpenwispVersion, '_post_metrics') @freeze_time('2023-12-01 00:00:00') def test_upgrade_not_detected_on_upgrade_event(self, mocked_post, *args): - """ - Tests send_usage_metrics is triggered with "Upgrade" category - but no modules were upgraded. - """ + """Tests send_usage_metrics is triggered with "Upgrade" category but no modules were upgraded.""" self.assertEqual(OpenwispVersion.objects.count(), 1) tasks.send_usage_metrics.delay(category='Upgrade') mocked_post.assert_not_called() diff --git a/openwisp_utils/qa.py b/openwisp_utils/qa.py index 97a44b72..b0ac6eee 100644 --- a/openwisp_utils/qa.py +++ b/openwisp_utils/qa.py @@ -1,21 +1,11 @@ -""" -Common Quality Assurance checks for OpenWISP modules -""" +"""Common Quality Assurance checks for OpenWISP modules.""" import argparse -import io import os import re import sys -from glob import iglob - -import docutils -import readme_renderer.rst as readme_rst def _parse_migration_check_args(): - """ - Parse and return CLI arguments - """ parser = argparse.ArgumentParser( description='Ensures migration files ' 'created have a descriptive name. If ' @@ -38,9 +28,9 @@ def _parse_migration_check_args(): def check_migration_name(): - """ - Ensure migration files created have a descriptive - name; if default name pattern is found, raise exception + """Ensure migration files created have a descriptive name. + + If the default name pattern is found, it will raise an exception. """ args = _parse_migration_check_args() if args.migrations_to_ignore is None: @@ -68,9 +58,6 @@ def check_migration_name(): def _parse_commit_check_args(): - """ - Parse and return CLI arguments - """ parser = argparse.ArgumentParser( description='Ensures the commit message ' 'follows the OpenWISP commit guidelines.' @@ -187,46 +174,14 @@ def check_commit_message(): sys.exit(1) -def check_rst_files(): - string_io = io.StringIO() - settings_overrides = {} - settings_overrides.update(readme_rst.SETTINGS) - settings_overrides['report_level'] = 2 - settings_overrides['warning_stream'] = string_io - files = [_ for _ in iglob('*.rst', recursive=True)] - for file in files: - data = read_rst_file(file) - try: - docutils.core.publish_string(data, settings_overrides=settings_overrides) - clean_body = readme_rst.clean(''.join(data)) - if clean_body is None: - # Raised sometimes when rendered failed to generate html - string_io.write("Failed to generate output for rendering") - raise ValueError("Output Failed") - except Exception: - pass - errors = string_io.getvalue().strip() - if errors: - body = 'There are some Errors in your RST file syntax \n\n' - for error in errors.splitlines(): - body += '- {}\n'.format(error) - print(body) - sys.exit(1) - - -def read_rst_file(filename): - with open(filename, 'r') as f: - data = f.read() - return data +def _find_issue_mentions(message): + """Looks for issue mentions in ``message``. + Returns a dict which contains: -def _find_issue_mentions(message): - """ - Looks for issue mentions in ``message`` - returns a dict which contains: - - list of mentioned issues - - count of mentions performed correctly - (using one of the common github keywords) + - List of mentioned issues. + - Count of mentions performed correctly (using one of the common + github keywords). """ words = message.split() issues = [] diff --git a/openwisp_utils/staticfiles.py b/openwisp_utils/staticfiles.py index 28be535b..38718365 100644 --- a/openwisp_utils/staticfiles.py +++ b/openwisp_utils/staticfiles.py @@ -9,10 +9,7 @@ class DependencyFinder(FileSystemFinder): - """ - A static files finder that finds static files of - django-apps listed in dependencies - """ + """Finds static files of apps listed in settings.EXTENDED_APPS.""" dependencies = list(EXTENDED_APPS) + ['openwisp_utils'] diff --git a/openwisp_utils/storage.py b/openwisp_utils/storage.py index a41374b6..53a64ebe 100644 --- a/openwisp_utils/storage.py +++ b/openwisp_utils/storage.py @@ -24,11 +24,6 @@ class CompressStaticFilesStorage( FileHashedNameMixin, BaseCompressStaticFilesStorage, ): - """ - A static files storage backend for compression that inherits from - django-compress-staticfiles's CompressStaticFilesStorage class; - also adds support for excluding file types using - "OPENWISP_STATICFILES_VERSIONED_EXCLUDE" setting. - """ + """Like CompressStaticFilesStorage, but allows excluding some files.""" pass diff --git a/openwisp_utils/test_selenium_mixins.py b/openwisp_utils/test_selenium_mixins.py index 12b1723c..abb74e57 100644 --- a/openwisp_utils/test_selenium_mixins.py +++ b/openwisp_utils/test_selenium_mixins.py @@ -7,9 +7,10 @@ class SeleniumTestMixin: - """ - A base test case for Selenium, providing helped methods for generating - clients and logging in profiles. + """A base Mixin Class for Selenium Browser Tests. + + Provides common initialization logic and helper methods like login() + and open(). """ admin_username = 'admin' @@ -47,11 +48,12 @@ def tearDownClass(cls): super().tearDownClass() def open(self, url, driver=None): - """ - Opens a URL - Argument: - url: URL to open - driver: selenium driver (default: cls.base_driver) + """Opens a URL. + + Input Arguments: + + - url: URL to open + - driver: selenium driver (default: cls.base_driver). """ if not driver: driver = self.web_driver @@ -61,12 +63,15 @@ def open(self, url, driver=None): ) def login(self, username=None, password=None, driver=None): - """ - Log in to the admin dashboard - Argument: - driver: selenium driver (default: cls.web_driver) - username: username to be used for login (default: cls.admin.username) - password: password to be used for login (default: cls.admin.password) + """Log in to the admin dashboard. + + Input Arguments: + + - username: username to be used for login (default: + cls.admin_username) + - password: password to be used for login (default: + cls.admin_password) + - driver: selenium driver (default: cls.web_driver). """ if not driver: driver = self.web_driver diff --git a/openwisp_utils/tests.py b/openwisp_utils/tests.py index f2ce1a9f..66486a80 100644 --- a/openwisp_utils/tests.py +++ b/openwisp_utils/tests.py @@ -14,9 +14,7 @@ @contextmanager def catch_signal(signal): - """ - Catches django signal and returns mock call for the same - """ + """Catches django signal and returns mock call for the same.""" handler = mock.Mock() signal.connect(handler) yield handler @@ -130,9 +128,7 @@ def __init__(self, stdout=None, stderr=None, **kwargs): class _AssertNumQueriesContextSubTest(CaptureQueriesContext): - """ - Needed to execute assertNumQueries in a subTest - """ + """Needed to execute assertNumQueries in a subTest.""" def __init__(self, test_case, num, connection): self.test_case = test_case diff --git a/openwisp_utils/utils.py b/openwisp_utils/utils.py index 49414211..9f81d6e8 100644 --- a/openwisp_utils/utils.py +++ b/openwisp_utils/utils.py @@ -18,9 +18,7 @@ def update(self, items): def get_random_key(): - """ - generates a random string of 32 characters - """ + """generates a random string of 32 characters.""" return get_random_string(length=32) @@ -33,9 +31,11 @@ def register_menu_items(items, name_menu='OPENWISP_DEFAULT_ADMIN_MENU_ITEMS'): def deep_merge_dicts(dict1, dict2): - """ - returns a new dict which is the result of the merge of the two dicts, - all elements are deepcopied to avoid modifying the original data structures + """Performs a deep merge of two dictionaries dict1 and dict2. + + Returns a new dict which is the result of the merge of the two dicts, + all elements are deepcopied to avoid modifying the original data + structures. """ result = deepcopy(dict1) for key, value in dict2.items(): @@ -52,8 +52,8 @@ def default_or_test(value, test): def print_color(string, color_name, end='\n'): - """ - Prints colored output on terminal from a selected range of colors. + """Prints colored output on terminal from a selected range of colors. + If color_name is not present then output won't be colored. """ color_dict = { diff --git a/setup.py b/setup.py index d7266d61..8eef0c5b 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ 'console_scripts': [ 'checkmigrations = openwisp_utils.qa:check_migration_name', 'checkcommit = openwisp_utils.qa:check_commit_message', - 'checkrst = openwisp_utils.qa:check_rst_files', ] }, scripts=['openwisp-qa-check', 'openwisp-qa-format', 'openwisp-pre-push-hook'], @@ -51,13 +50,12 @@ ], extras_require={ 'qa': [ - 'black~=23.3.0', - 'flake8~=6.0.0', - 'isort~=5.10.1', - 'readme-renderer~=37.3.0', + 'black~=23.12.1', + 'flake8~=7.1.0', + 'isort~=5.13.2', 'coveralls~=4.0.1', # depends on coverage as well 'tblib~=3.0.0', - 'docstrfmt~=1.7.0', + 'docstrfmt~=1.8.0', ], 'rest': [ 'djangorestframework~=3.14.0', @@ -65,7 +63,7 @@ 'drf-yasg~=1.21.7', ], 'celery': ['celery~=5.3.0'], - 'selenium': ['selenium>=4.10,<4.23'], + 'selenium': ['selenium>=4.10,<4.24'], }, classifiers=[ 'Development Status :: 5 - Production/Stable ', diff --git a/tests/test_project/api/throttling.py b/tests/test_project/api/throttling.py index 562adb9c..c6f6003b 100644 --- a/tests/test_project/api/throttling.py +++ b/tests/test_project/api/throttling.py @@ -2,8 +2,6 @@ class CustomScopedRateThrottle(ScopedRateThrottle): - """ - Used only for automated testing purposes - """ + """Used only for automated testing purposes.""" pass diff --git a/tests/test_project/api/views.py b/tests/test_project/api/views.py index ad8bec47..5e561cb8 100644 --- a/tests/test_project/api/views.py +++ b/tests/test_project/api/views.py @@ -6,10 +6,10 @@ class ReceiveProjectView(View): - """ - This test view allows to check the validity of the pk and the key, and receive project name - Required query string parameters: - * key + """Test View. + + Test view is used to check the validity of the pk and the key. It + returns the project name. """ def get(self, request, pk): diff --git a/tests/test_project/tests/test_qa.py b/tests/test_project/tests/test_qa.py index 17bd8701..59328e71 100644 --- a/tests/test_project/tests/test_qa.py +++ b/tests/test_project/tests/test_qa.py @@ -1,14 +1,9 @@ import os from os import path -from unittest.mock import Mock, patch +from unittest.mock import patch from django.test import TestCase -from openwisp_utils.qa import ( - check_commit_message, - check_migration_name, - check_rst_files, - read_rst_file, -) +from openwisp_utils.qa import check_commit_message, check_migration_name from openwisp_utils.tests import capture_stderr, capture_stdout MIGRATIONS_DIR = path.join( @@ -252,31 +247,3 @@ def test_qa_call_check_commit_message_bump_version(self): except (SystemExit, Exception) as e: msg = 'Check failed:\n\n{}\n\nOutput:{}'.format(option[-1], e) self.fail(msg) - - def test_qa_call_check_rst_file(self): - try: - read_rst_file(self._test_rst_file) - except (SystemExit, Exception) as e: - msg = 'Check failed:\n\nOutput:{}'.format(e) - self.fail(msg) - - @patch('readme_renderer.rst.clean', Mock(return_value=None)) - # Here the value is mocked because the error occurs in some versions of library only - @capture_stdout() - def test_qa_call_check_rst_file_clean_failure(self, captured_output): - try: - check_rst_files() - except ValueError: - message = 'Output Failed' - self.assertIn(message, captured_output.getvalue()) - except SystemExit: - pass - else: - self.fail('SystemExit not raised') - - @capture_stdout() - def test_qa_call_check_rst_file_syntax(self): - with open(self._test_rst_file, 'a+') as f: - f.write('Test File \n======= \n.. code:: python') - with self.assertRaises(SystemExit): - check_rst_files() diff --git a/tests/test_project/tests/test_selenium.py b/tests/test_project/tests/test_selenium.py index d8c2381a..c1e069eb 100644 --- a/tests/test_project/tests/test_selenium.py +++ b/tests/test_project/tests/test_selenium.py @@ -219,15 +219,16 @@ def _test_popup_page(self): self.open(reverse('admin:index')) def test_addition_of_transition_effect(self): - transition = 'none 0s ease 0s' + # On Github Actions CI is resulting in a slightly different value + transitions = ['none 0s ease 0s', 'none'] # none because transition has been set to none during tests self.login() menu = self.web_driver.find_element(By.ID, 'menu') main_content = self._get_main_content() menu_toggle = self._get_menu_toggle() - self.assertEqual(menu.value_of_css_property('transition'), transition) - self.assertEqual(main_content.value_of_css_property('transition'), transition) - self.assertEqual(menu_toggle.value_of_css_property('transition'), transition) + self.assertIn(menu.value_of_css_property('transition'), transitions) + self.assertIn(main_content.value_of_css_property('transition'), transitions) + self.assertIn(menu_toggle.value_of_css_property('transition'), transitions) def test_menu_on_wide_screen(self): self.login() @@ -271,11 +272,7 @@ def test_menu_on_wide_screen(self): self._test_login_and_logout_page() def test_active_menu_group(self): - """ - Test active menu group: - - Active group should close only when clicked on menu else - it should remain open. - """ + """The active group should close only when the menu, is clicked, otherwise it should remain open.""" self.login() url = reverse('admin:auth_user_changelist') self.open(url) @@ -746,9 +743,7 @@ def test_autocomplete_shelf_filter(self): self.assertNotEqual(option.text, '-') def test_autocomplete_owner_filter(self): - """ - Tests the null option of the AutocompleteFilter - """ + """Tests the null option of the AutocompleteFilter.""" url = reverse('admin:test_project_shelf_changelist') user = self._create_user() horror_shelf = self._create_shelf( diff --git a/tests/test_project/tests/test_storage.py b/tests/test_project/tests/test_storage.py index 5090efea..29eb70e6 100644 --- a/tests/test_project/tests/test_storage.py +++ b/tests/test_project/tests/test_storage.py @@ -9,10 +9,11 @@ def create_dir(*paths: str): - """Returns Joined path + """Returns joined path from input arguments. - joins two or more pathname using os.path.join and creates leaf directory - and all the intermidiate ones according to the joined path using os.makedirs + joins two or more pathname using os.path.join and creates leaf + directory and all the intermidiate ones according to the joined path + using os.makedirs. """ joined_path = os.path.join(*paths) os.makedirs(joined_path, exist_ok=True) diff --git a/tests/test_project/tests/utils.py b/tests/test_project/tests/utils.py index 33067232..50d9ec1f 100644 --- a/tests/test_project/tests/utils.py +++ b/tests/test_project/tests/utils.py @@ -16,9 +16,7 @@ class TestConfigMixin(object): - """ - Get the configurations that are to be used for all the tests. - """ + """Loads test configuration from a config.json file.""" config_file = os.path.join(os.path.dirname(__file__), 'config.json') root_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')