Package: openwisp-utils
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!Current featuresConfigurable admin themeOpenWISP DashboardConfigurable navigation menuImproved admin filtersOpenAPI / Swagger documentationModel utilitiesStorage utilitiesAdmin utilitiesCode utilitiesAdmin Theme utilitiesREST API utilitiesTest utilitiesQuality assurance checksTable of Contents:Current featuresInstall stable version from pypiInstall development versionUsing theadmin_themeUsingDependencyLoaderandDependencyFinderDependencyFinderDependencyLoaderSupplying custom CSS and JS for the admin themeOpenWISP Dashboardregister_dashboard_templateunregister_dashboard_templateregister_dashboard_chartunregister_dashboard_chartMain navigation menuregister_menu_groupAdding a custom linkAdding a model linkAdding a menu groupregister_menu_subitemHow to use custom icons in the menuAdmin filtersModel utilitiesopenwisp_utils.base.UUIDModelopenwisp_utils.base.TimeStampedEditableModelopenwisp_utils.base.KeyFieldAdmin utilitiesopenwisp_utils.admin.TimeReadonlyAdminMixinopenwisp_utils.admin.ReadOnlyAdminopenwisp_utils.admin.AlwaysHasChangedMixinopenwisp_utils.admin.UUIDAdminopenwisp_utils.admin.ReceiveUrlAdminopenwisp_utils.admin.HelpTextStackedInlineopenwisp_utils.admin_theme.filters.InputFilteropenwisp_utils.admin_theme.filters.SimpleInputFilterCode utilitiesopenwisp_utils.utils.get_random_keyopenwisp_utils.utils.deep_merge_dictsopenwisp_utils.utils.default_or_testopenwisp_utils.utils.print_coloropenwisp_utils.utils.SorrtedOrderedDictopenwisp_utils.tasks.OpenwispCeleryTaskStorage utilitiesAdmin Theme utilitiesREST API utilitiesopenwisp_utils.api.serializers.ValidatedModelSerializeropenwisp_utils.api.apps.ApiAppConfigTest utilitiesopenwisp_utils.tests.catch_signalopenwisp_utils.tests.TimeLoggingTestRunneropenwisp_utils.tests.capture_stdoutopenwisp_utils.tests.capture_stderropenwisp_utils.tests.capture_any_outputopenwisp_utils.tests.AssertNumQueriesSubTestMixinQuality Assurance Checksopenwisp-qa-formatopenwisp-qa-checkcheckmigrationscheckcommitcheckendlinecheckpendingmigrationscheckrstSettingsOPENWISP_ADMIN_SITE_CLASSOPENWISP_ADMIN_SITE_TITLEOPENWISP_ADMIN_SITE_HEADEROPENWISP_ADMIN_INDEX_TITLEOPENWISP_ADMIN_DASHBOARD_ENABLEDOPENWISP_ADMIN_THEME_LINKSOPENWISP_ADMIN_THEME_JSOPENWISP_ADMIN_SHOW_USERLINKS_BLOCKOPENWISP_API_DOCSOPENWISP_API_INFOOPENWISP_SLOW_TEST_THRESHOLDOPENWISP_STATICFILES_VERSIONED_EXCLUDEOPENWISP_HTML_EMAILOPENWISP_EMAIL_TEMPLATEOPENWISP_EMAIL_LOGOOPENWISP_CELERY_SOFT_TIME_LIMITOPENWISP_CELERY_HARD_TIME_LIMITInstalling for developmentContributingSupportChangelogLicenseInstall stable version from pypiInstall from pypi:pipinstallopenwisp-utils# install optional dependencies for REST frameworkpipinstallopenwisp-utils[rest]# install optional dependencies for tests (flake8, black and isort)pipinstallopenwisp-utils[qa]# or install everythingpipinstallopenwisp-utils[rest,qa]Install development versionInstall tarball:pipinstallhttps://github.com/openwisp/openwisp-utils/tarball/masterAlternatively you can install via pip using git:pipinstall-egit+git://github.com/openwisp/openwisp-utils#egg=openwisp-utilsUsing theadmin_themeThe admin theme requires Django >= 2.2..Addopenwisp_utils.admin_themetoINSTALLED_APPSinsettings.py: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.sites',# admin'django.contrib.admin',]UsingDependencyLoaderandDependencyFinderAdd the list of all packages extended toEXTENDED_APPSinsettings.py.For example, if you’ve extendeddjango_x509:EXTENDED_APPS=['django_x509']DependencyFinderThis is a static finder which looks for static files in thestaticdirectory of the apps listed insettings.EXTENDED_APPS.Addopenwisp_utils.staticfiles.DependencyFindertoSTATICFILES_FINDERSinsettings.py.STATICFILES_FINDERS=['django.contrib.staticfiles.finders.FileSystemFinder','django.contrib.staticfiles.finders.AppDirectoriesFinder','openwisp_utils.staticfiles.DependencyFinder',# <----- add this]DependencyLoaderThis is a template loader which looks for templates in thetemplatesdirectory of the apps listed insettings.EXTENDED_APPS.Addopenwisp_utils.loaders.DependencyLoaderto
templateloadersinsettings.pyas shown below.TEMPLATES=[{'BACKEND':'django.template.backends.django.DjangoTemplates','DIRS':[],'OPTIONS':{'loaders':[# ... other loaders ...'openwisp_utils.loaders.DependencyLoader',# <----- add this],'context_processors':[# ... omitted ...],},},]Supplying custom CSS and JS for the admin themeAddopenwisp_utils.admin_theme.context_processor.admin_theme_settingsto
templatecontext_processorsinsettings.pyas shown below.
This will allow to setOPENWISP_ADMIN_THEME_LINKSandOPENWISP_ADMIN_THEME_JSsettings
to provide CSS and JS files to customise admin theme.TEMPLATES=[{'BACKEND':'django.template.backends.django.DjangoTemplates','DIRS':[],'OPTIONS':{'loaders':[# ... omitted ...],'context_processors':[# ... other context processors ...'openwisp_utils.admin_theme.context_processor.admin_theme_settings'# <----- add this],},},]NoteYou 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 theSTATICFILES_DIRsetting insettings.py.You can learn more in theDjango documentation.OpenWISP DashboardTheadmin_themesub app of this package provides an admin dashboard
for OpenWISP which can be manipulated with the functions described in
the next sections.Example 1, monitoring:Example 2, controller:register_dashboard_templateAllows including a specific django template in the OpenWISP dashboard.It is designed to allow the inclusion of the geographic map
shipped byOpenWISP Monitoringbut can be used to include any custom element in the dashboard.Note: templates are loaded before charts.Syntax:register_dashboard_template(position,config)ParameterDescriptionposition(int) The position of the template.config(dict) The configuration of the template.extra_configoptional(dict) Extra configuration you want to pass to custom template.Following properties can be configured for each templateconfig:PropertyDescriptiontemplate(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:fromopenwisp_utils.admin_themeimportregister_dashboard_templateregister_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',),'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',},)It is recommended to register dashboard templates from thereadymethod of the AppConfig of the app where the templates are defined.unregister_dashboard_templateThis function can be used to remove a template from the dashboard.Syntax:unregister_dashboard_template(template_name)ParameterDescriptiontemplate_name(str) The name of the template to remove.Code example:fromopenwisp_utils.admin_themeimportunregister_dashboard_templateunregister_dashboard_template('admin/dashboard/device_map.html')Note: anImproperlyConfiguredexception is raised the
specified dashboard template is not registered.register_dashboard_chartAdds 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.Syntax:register_dashboard_chart(position,config)ParameterDescriptionposition(int) Position of the chart.config(dict) Configuration of chart.Following properties can be configured for each chartconfig:PropertyDescriptionquery_paramsIt is a required property in form ofdictcontaining following properties:PropertyDescriptionname(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.annotateAlternative togroup_by,dictused for more complex queries.aggregateAlternative togroup_by,dictused for more complex queries.colorsAnoptionaldictwhich can be used to define colors for each distinct
value shown in the pie charts.labelsAnoptionaldictwhich 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.filtersAnoptionaldictwhich can be used when usingaggregateandannotateinquery_paramsto 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).Code example:fromopenwisp_utils.admin_themeimportregister_dashboard_chartregister_dashboard_chart(position=1,config={'query_params':{'name':'Operator Project Distribution','app_label':'test_project','model':'operator','group_by':'project__name',},'colors':{'Utils':'red','User':'orange'},},)For real world examples, look at the code ofOpenWISP ControllerandOpenWISP Monitoring.Note: anImproperlyConfiguredexception is raised if a
dashboard element is already registered at same position.It is recommended to register dashboard charts from thereadymethod
of the AppConfig of the app where the models are defined.
Checkoutapp.py of the test_projectfor reference.unregister_dashboard_chartThis function can used to remove a chart from the dashboard.Syntax:unregister_dashboard_chart(chart_name)ParameterDescriptionchart_name(str) The name of the chart to remove.Code example:fromopenwisp_utils.admin_themeimportunregister_dashboard_chartunregister_dashboard_chart('Operator Project Distribution')Note: anImproperlyConfiguredexception is raised the
specified dashboard chart is not registered.Main navigation menuTheadmin_themesub app of this package provides a navigation menu that can be
manipulated with the functions described in the next sections.Addopenwisp_utils.admin_theme.context_processor.menu_groupsto
templatecontext_processorsinsettings.pyas shown below.TEMPLATES=[{'BACKEND':'django.template.backends.django.DjangoTemplates','DIRS':[],'OPTIONS':{'loaders':[# ... omitted ...],'context_processors':[# ... other context processors ...'openwisp_utils.admin_theme.context_processor.menu_groups'# <----- add this],},},]register_menu_groupAllows registering a new menu item or group at the specified position in the Main Navigation Menu.Syntax:register_menu_group(position,config)ParameterDescriptionposition(int) Position of the group or item.config(dict) Configuration of the goup or item.Code example:fromdjango.utils.translationimportugettext_lazyas_fromopenwisp_utils.admin_theme.menuimportregister_menu_groupregister_menu_group(position=1,config={'label':_('My Group'),'items':{1:{'label':_('Users List'),'model':'auth.User','name':'changelist','icon':'list-icon',},2:{'label':_('Add User'),'model':'auth.User','name':'add','icon':'add-icon',},},'icon':'user-group-icon',},)register_menu_group(position=2,config={'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'})NoteAnImproperlyConfiguredexception is raised if a menu element is already registered at the same position.AnImproperlyConfiguredexception 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 useregister_menu_groupin thereadymethod of theAppConfig.register_menu_itemsis obsoleted byregister_menu_groupand will be removed in
future versions. Links added usingregister_menu_itemswill be shown at the top
of navigation menu and above anyregister_menu_groupitems.Adding a custom linkTo add a link that contains a custom URL the following syntax can be used.Syntax:register_menu_group(position=1,config={"label":"Link Label","url":"link_url","icon":"my-icon"})Following is the description of the configuration:ParameterDescriptionlabel(str) Display text for the link.url(str) url for the link.iconAnoptionalstrCSS class name for the icon. No icon
is displayed if not provided.Adding a model linkTo 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.Syntax:# add a link of list pageregister_menu_group(position=1,config={'model':'my_project.MyModel','name':'changelist','label':'MyModel List','icon':'my-model-list-class',},)# add a link of add pageregister_menu_group(position=2,config={'model':'my_project.MyModel','name':'add','label':'MyModel Add Item','icon':'my-model-add-class',},)Following is the description of the configuration:ParameterDescriptionmodel(str) Model of the app for which you to add link.name(str) url name. eg. changelist or add.labelAnoptionalstrdisplay text for the link. It is
automatically generated if not provided.iconAnoptionalstrCSS class name for the icon. No icon
is displayed if not provided.Adding a menu groupTo add a nested group of links in the menu the following syntax can be used.
It creates a dropdown in the menu.Syntax:register_menu_group(position=1,config={'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',},},'icon':'my-group-icon-class',},)Following is the description of the configuration:ParameterDescriptionlabel(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.iconAnoptionalstrCSS class name for the icon. No icon
is displayed if not provided.register_menu_subitemAllows adding an item to a registered group.Syntax:register_menu_subitem(group_position,item_position,config)ParameterDescriptiongroup_position(int) Position of the group in which item should be added.item_position(int) Position at which item should be added in the groupconfig(dict) Configuration of the item.Code example:fromdjango.utils.translationimportugettext_lazyas_fromopenwisp_utils.admin_theme.menuimportregister_menu_subitem# To register a model linkregister_menu_subitem(group_position=10,item_position=2,config={'label':_('Users List'),'model':'auth.User','name':'changelist','icon':'list-icon',},)# To register a custom linkregister_menu_subitem(group_position=10,item_position=2,config={'label':_('My Link'),'url':'https://link.com'},)NoteAnImproperlyConfiguredexception is raised if the group is not already
registered atgroup_position.AnImproperlyConfiguredexception is raised if the group already has an
item registered atitem_position.It is only possible to register links to specific models or custom URL.
AnImproperlyConfiguredexception is raised if the configuration of
group is provided in the function.It is recommended to useregister_menu_subitemin thereadymethod of theAppConfig.How to use custom icons in the menuCreate 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 theiconparameter used in the configuration of a menu item or group. Also icon being used
should be insvgformat.Example:.icon-class-name:{mask-image:url(imageurl);-webkit-mask-image:url(imageurl);}Follow the instructions inSupplying custom CSS and JS for the admin themeto know how to configure your OpenWISP instance to load custom CSS files.Admin filtersTheadmin_themesub 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).Model utilitiesopenwisp_utils.base.UUIDModelModel class which provides a UUID4 primary key.openwisp_utils.base.TimeStampedEditableModelModel class inheritingUUIDModelwhich provides two additional fields:createdmodifiedWhich use respectivelyAutoCreatedField,AutoLastModifiedFieldfrommodel_utils.fields(self-updating fields providing the creation date-time and the last modified date-time).openwisp_utils.base.KeyFieldA model field whic provides a random key or token, widely used across openwisp modules.Admin utilitiesopenwisp_utils.admin.TimeReadonlyAdminMixinAdmin mixin which adds two readonly fieldscreatedandmodified.This is an admin mixin for models inheritingTimeStampedEditableModelwhich adds the fieldscreatedandmodifiedto the database.openwisp_utils.admin.ReadOnlyAdminA read-onlyModelAdminbase class.Will include theidfield by default, which can be excluded by supplying
theexcludeattribute, eg:fromopenwisp_utils.adminimportReadOnlyAdminclassPostAuthReadOnlyAdmin(ReadOnlyAdmin):exclude=['id']openwisp_utils.admin.AlwaysHasChangedMixinA 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.UUIDAdminAn admin class that provides the UUID of the object as a read-only input field
(to make it easy and quick to copy/paste).openwisp_utils.admin.ReceiveUrlAdminAn admin class that provides an URL as a read-only input field
(to make it easy and quick to copy/paste).openwisp_utils.admin.HelpTextStackedInlineA stacked inline admin class that displays a help text for entire
inline object. Following is an example:fromopenwisp_utils.adminimportHelpTextStackedInlineclassSubnetDivisionRuleInlineAdmin(MultitenantAdminMixin,TimeReadonlyAdminMixin,HelpTextStackedInline):model=Model# It is required to set "help_text" attributehelp_text={# (required) Help text to display'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')# (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'}openwisp_utils.admin_theme.filters.InputFilterTheadmin_themesub app of this package provides an input filter that can be used in changelist page
to filterUUIDFieldorCharField.Code example:fromdjango.contribimportadminfromopenwisp_utils.admin_theme.filtersimportInputFilterfrommy_app.modelsimportMyModel@admin.register(MyModel)classMyModelAdmin(admin.ModelAdmin):list_filter=[('my_field',InputFilter),'other_field'...]By defaultInputFilteruse exact lookup to filter items which matches to the value being
searched by the user. But this behavior can be changed by modifyingInputFilteras following:fromdjango.contribimportadminfromopenwisp_utils.admin_theme.filtersimportInputFilterfrommy_app.modelsimportMyModelclassMyInputFilter(InputFilter):lookup='icontains'@admin.register(MyModel)classMyModelAdmin(admin.ModelAdmin):list_filter=[('my_field',MyInputFilter),'other_field'...]To know about other lookups that can be used please checkDjango Lookup API Referenceopenwisp_utils.admin_theme.filters.SimpleInputFilterA stripped down version ofopenwisp_utils.admin_theme.filters.InputFilterthat provides
flexibility to customize filtering. It can be used to filter objects using indirectly
related fields.The derived filter class should define thequerysetmethod as shown in following example:fromdjango.contribimportadminfromopenwisp_utils.admin_theme.filtersimportSimpleInputFilterfrommy_app.modelsimportMyModelclassMyInputFilter(SimpleInputFilter):parameter_name='shelf'title=_('Shelf')defqueryset(self,request,queryset):ifself.value()isnotNone:returnqueryset.filter(name__icontains=self.value())@admin.register(MyModel)classMyModelAdmin(admin.ModelAdmin):list_filter=[MyInputFilter,'other_field'...]Code utilitiesopenwisp_utils.utils.get_random_keyGenerates an random string of 32 characters.openwisp_utils.utils.deep_merge_dictsReturns a newdictwhich is the result of the merge of the two dictionaries,
all elements are deep-copied to avoid modifying the original data structures.Usage:fromopenwisp_utils.utilsimportdeep_merge_dictsmergd_dict=deep_merge_dicts(dict1,dict2)openwisp_utils.utils.default_or_testIf the program is being executed during automated tests the value supplied in
thetestargument will be returned, otherwise the one supplied in thevalueargument is returned.fromopenwisp_utils.utilsimportdefault_or_testTHROTTLE_RATE=getattr(settings,'THROTTLE_RATE',default_or_test(value='20/day',test=None),)openwisp_utils.utils.print_colordefault colors:['white_bold', 'green_bold', 'yellow_bold', 'red_bold']If you want to print a string inRed Bold, you can do it as below.fromopenwisp_utils.utilsimportprint_colorprint_color('This is the printed in Red Bold',color_name='red_bold')You may also provide theendarguement similar to built-in print method.openwisp_utils.utils.SorrtedOrderedDictExtendscollections.SortedDictand implements logic to sort inserted
items based onkeyvalue. Sorting is done at insert operation which
incurs memory space overhead.openwisp_utils.tasks.OpenwispCeleryTaskA custom celery task class that sets hard and soft time limits of celery tasks
usingOPENWISP_CELERY_HARD_TIME_LIMITandOPENWISP_CELERY_SOFT_TIME_LIMITsettings respectively.Usage:fromceleryimportshared_taskfromopenwisp_utils.tasksimportOpenwispCeleryTask@shared_task(base=OpenwispCeleryTask)defyour_celery_task():passNote: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).Storage utilitiesopenwisp_utils.storage.CompressStaticFilesStorageA static storage backend for compression inheriting fromdjango-compress-staticfiles’sCompressStaticFilesStorageclass.Adds support for excluding file types usingOPENWISP_STATICFILES_VERSIONED_EXCLUDEsetting.To use pointSTATICFILES_STORAGEtoopenwisp_utils.storage.CompressStaticFilesStorageinsettings.py.STATICFILES_STORAGE='openwisp_utils.storage.CompressStaticFilesStorage'Admin Theme utilitiesopenwisp_utils.admin_theme.email.send_emailThis function allows sending email in both plain text and HTML version (using the template
and logo that can be customised usingOPENWISP_EMAIL_TEMPLATEandOPENWISP_EMAIL_LOGOrespectively).In case the HTML version if not needed it may be disabled by
settingOPENWISP_HTML_EMAILtoFalse.Syntax:send_email(subject,body_text,body_html,recipients)ParameterDescriptionsubject(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_contextoptional(dict) Extra context which is passed to the template.
The dictionary keyscall_to_action_textandcall_to_action_urlcan be passed to show a call to action button.
Similarly,footercan be passed to add a footer.Note: Data passed in body should be validated and user supplied data should not be sent directly to the function.REST API utilitiesopenwisp_utils.api.serializers.ValidatedModelSerializerA model serializer which calls the model instancefull_clean().openwisp_utils.api.apps.ApiAppConfigIf 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:fromdjango.confimportsettingsfromdjango.utils.translationimportugettext_lazyas_fromopenwisp_utils.api.appsimportApiAppConfigclassMyModuleConfig(ApiAppConfig):name='my_openwisp_module'label='my_module'verbose_name=_('My OpenWISP Module')# assumes API is enabled by defaultAPI_ENABLED=getattr(settings,'MY_OPENWISP_MODULE_API_ENABLED',True)# set throttling rates for your module hereREST_FRAMEWORK_SETTINGS={'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.Test utilitiesopenwisp_utils.tests.catch_signalThis method can be used to mock a signal call inorder to easily verify
that the signal has been called.Usage example as a context-manager:fromopenwisp_utils.testsimportcatch_signalwithcatch_signal(openwisp_signal)ashandler:model_instance.trigger_signal()handler.assert_called_once_with(arg1='value1',arg2='value2',sender=ModelName,signal=openwisp_signal,)openwisp_utils.tests.TimeLoggingTestRunnerThis class extends thedefault test runner provided by Djangoand 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 withOPENWISP_SLOW_TEST_THRESHOLD.In order to switch to this test runner you have set the following in yoursettings.py:TEST_RUNNER='openwisp_utils.tests.TimeLoggingTestRunner'openwisp_utils.tests.capture_stdoutThis decorator can be used to capture standard output produced by tests,
either to silence it or to write assertions.Example usage:fromopenwisp_utils.testsimportcapture_stdout@capture_stdout()deftest_something(self):function_generating_output()# pseudo code@capture_stdout()deftest_something_again(self,captured_ouput):# pseudo codefunction_generating_output()# now you can create assertions on the captured outputself.assertIn('expected stdout',captured_ouput.getvalue())# if there are more than one assertions, clear the captured output firstcaptured_error.truncate(0)captured_error.seek(0)# you can create new assertion nowself.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 namedcaptured_output) can be passed as an argument
to the decorated test method, alternatively it can be omitted.AStingIOinstance is used for capturing output by default but if needed
it’s possible to pass a customStringIOinstance to the decorator function.openwisp_utils.tests.capture_stderrEquivalent tocapture_stdout, but for standard error.Example usage:fromopenwisp_utils.testsimportcapture_stderr@capture_stderr()deftest_error(self):function_generating_error()# pseudo code@capture_stderr()deftest_error_again(self,captured_error):# pseudo codefunction_generating_error()# now you can create assertions on captured errorself.assertIn('expected error',captured_error.getvalue())# if there are more than one assertions, clear the captured error firstcaptured_error.truncate(0)captured_error.seek(0)# you can create new assertion nowself.assertIn('another expected error',captured_error.getvalue())openwisp_utils.tests.capture_any_outputEquivalent tocapture_stdoutandcapture_stderr, but captures both types of
output (standard output and standard error).Example usage:fromopenwisp_utils.testsimportcapture_any_output@capture_any_output()deftest_something_out(self):function_generating_output()# pseudo code@capture_any_output()deftest_out_again(self,captured_output,captured_error):# pseudo codefunction_generating_output_and_errors()# now you can create assertions on captured errorself.assertIn('expected stdout',captured_output.getvalue())self.assertIn('expected stderr',captured_error.getvalue())openwisp_utils.tests.AssertNumQueriesSubTestMixinThis mixin overrides theassertNumQueriesassertion from the django test case to run in asubTestso that the
query check does not block the whole test if it fails.Example usage:fromdjango.testimportTestCasefromopenwisp_utils.testsimportAssertNumQueriesSubTestMixinclassMyTest(AssertNumQueriesSubTestMixin,TestCase):defmy_test(self):withself.assertNumQueries(2):MyModel.objects.count()# the assertion above will fail but this line will be executedprint('This will be printed anyway.')Quality Assurance ChecksThis package contains some common QA checks that are used in the
automated builds of different OpenWISP modules.openwisp-qa-formatThis shell script automatically formats Python and CSS code according
to theOpenWISP coding style conventions.It runsisortandblackto format python code
(these two dependencies are required and installed automatically when runningpip installopenwisp-utils[qa]).Thestylelintandjshintprograms are used to perform style checks on CSS and JS code respectively, but they are optional:
ifstylelintand/orjshintare not installed, the check(s) will be skipped.openwisp-qa-checkShell script to run the following quality assurance checks:checkmigrationscheckcommitcheckendlinecheckpendingmigrationscheckrstflake8- Python code linterisort- Sorts python imports alphabetically, and seperated into sectionsblack- Formats python code using a common standardcsslinter- Formats and checks CSS code using stylelint common standardjslinter- Checks Javascript code using jshint common standardIf a check requires a flag, it can be passed forward in the same way.Usage example:openwisp-qa-check --migration-path <path> --message <commit-message>Any unneeded checks can be skipped by passing--skip-<check-name>Usage example:openwisp-qa-check --skip-isortFor backward compatibilitycsslinterandjslinterare skipped by default.
To run them in checks pass arguements in this way.Usage example:# To activate csslinter
openwisp-qa-check --csslinter

# To activate jslinter
openwisp-qa-check --jslinterYou can do multiplecheckmigrationsby passing the arguments with space-delimited string.For example, this multiplecheckmigrations:checkmigrations --migrations-to-ignore 3 \
        --migration-path ./openwisp_users/migrations/ || exit 1

checkmigrations --migrations-to-ignore 2 \
        --migration-path ./tests/testapp/migrations/ || exit 1Can be changed with:openwisp-qa-check --migrations-to-ignore "3 2" \
        --migration-path "./openwisp_users/migrations/ ./tests/testapp/migrations/"checkmigrationsEnsures the latest migrations created have a human readable name.We want to avoid having many migrations named like0003_auto_20150410_3242.py.This way we can reconstruct the evolution of our database schemas faster, with
less efforts and hence less costs.Usage example:checkmigrations --migration-path ./django_freeradius/migrations/checkcommitEnsures the last commit message follows ourcommit 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.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#noqato the end of your commit message.Usage example:[qa] Improved #20

Simulation of a special unplanned case
#noqacheckendlineEnsures that a blank line is kept at the end of each file.checkpendingmigrationsEnsures there django migrations are up to date and no new migrations need to
be created.It accepts an optional--migration-moduleflag indicating the django app
name that should be passed to./manage.py makemigrations, eg:./manage.py makemigrations $MIGRATION_MODULE.checkrstChecks the syntax of all ReStructuredText files to ensure they can be published on pypi or using python-sphinx.SettingsOPENWISP_ADMIN_SITE_CLASSdefault:openwisp_utils.admin_theme.admin.OpenwispAdminSiteIf you need to use a customized admin site class, you can use this setting.OPENWISP_ADMIN_SITE_TITLEdefault:OpenWISP AdminTitle value used in the<title>HTML tag of the admin site.OPENWISP_ADMIN_SITE_HEADERdefault:OpenWISPHeading text used in the main<h1>HTML tag (the logo) of the admin site.OPENWISP_ADMIN_INDEX_TITLEdefault:Network administrationTitle shown to users in the index page of the admin site.OPENWISP_ADMIN_DASHBOARD_ENABLEDdefault:TrueWhenTrue, enables theOpenWISP Dashboard.
Upon login, the user will be greeted with the dashboard instead of the default
Django admin index page.OPENWISP_ADMIN_THEME_LINKSdefault:[]Note: this setting requiresthe admin_theme_settings context processorin 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.The following example shows how to keep using the default CSS,
supply an additional CSS and replace the favicon.Example usage: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'}]OPENWISP_ADMIN_THEME_JSdefault:[]Allows to pass a list of strings representing URLs of custom JS files to load.Example usage:OPENWISP_ADMIN_THEME_JS=['/static/custom-admin-theme.js',]OPENWISP_ADMIN_SHOW_USERLINKS_BLOCKdefault:FalseWhen 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.OPENWISP_API_DOCSdefault:TrueWhether the OpenAPI documentation is enabled.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:urlpatterns+=[url(r'^api/v1/',include('openwisp_utils.api.urls')),]OPENWISP_API_INFOdefault:{'title':'OpenWISP API','default_version':'v1','description':'OpenWISP REST API',}Define OpenAPI general information.
NOTE: This setting requiresOPENWISP_API_DOCS = Trueto take effect.For more information about optional parameters check thedrf-yasg documentation.OPENWISP_SLOW_TEST_THRESHOLDdefault:[0.3, 1](seconds)It can be used to change the thresholds used byTimeLoggingTestRunnerto detect slow tests (0.3s by default) and highlight the slowest ones (1s by default) amongst them.OPENWISP_STATICFILES_VERSIONED_EXCLUDEdefault:['leaflet/*/*.png']Allows to pass a list ofUnix shell-style wildcardsfor files to be excluded byCompressStaticFilesStorage.By default Leaflet PNGs have been excluded to avoid bugs likeopenwisp/ansible-openwisp2#232.Example usage:OPENWISP_STATICFILES_VERSIONED_EXCLUDE=['*png',]OPENWISP_HTML_EMAILtypebooldefaultTrueIfTrue, an HTML themed version of the email can be sent using
thesend_emailfunction.OPENWISP_EMAIL_TEMPLATEtypestrdefaultopenwisp_utils/email_template.htmlThis setting allows to change the django template used for sending emails with
thesend_emailfunction.
It is recommended to extend the default email template as in the example below.{%extends'openwisp_utils/email_template.html'%}{%blockstyles%}{{block.super}}<style>
  .background {
    height: 100%;
    background: linear-gradient(to bottom, #8ccbbe 50%, #3797a4 50%);
    background-repeat: no-repeat;
    background-attachment: fixed;
    padding: 50px;
  }

  .mail-header {
    background-color: #3797a4;
    color: white;
  }
</style>{%endblockstyles%}Similarly, you can customize the HTML of the template by overriding thebodyblock.
Seeemail_template.htmlfor reference implementation.OPENWISP_EMAIL_LOGOtypestrdefaultOpenWISP logoThis 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.OPENWISP_CELERY_SOFT_TIME_LIMITtypeintdefault30(in seconds)Sets the soft time limit for celery tasks usingOpenwispCeleryTask.OPENWISP_CELERY_HARD_TIME_LIMITtypeintdefault120(in seconds)Sets the hard time limit for celery tasks usingOpenwispCeleryTask.Installing for developmentInstall the system dependencies:sudoapt-getinstallsqlite3libsqlite3-dev# For running E2E Selenium testssudoaptinstallchromiumInstall your forked repo:gitclonegit://github.com/<your_fork>/openwisp-utilscdopenwisp-utils/pipinstall-e.[qa,rest]Install test requirements:pipinstall-rrequirements-test.txtInstall node dependencies used for testing:npminstall-gstylelintjshintSet 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:openwisp-pre-push-hook--installInstall WebDriver for Chromium for your browser version fromhttps://chromedriver.chromium.org/homeand Extractchromedriverto one of directories from your$PATH(example:~/.local/bin/).Create database:cdtests/./manage.pymigrate./manage.pycreatesuperuserRun development server:cdtests/./manage.pyrunserverYou can access the admin interface of the test project athttp://127.0.0.1:8000/admin/.Run tests with:./runtests.py--parallelContributingPlease refer to theOpenWISP contributing guidelines.SupportSeeOpenWISP Support Channels.ChangelogSeeCHANGES.LicenseSeeLICENSE.