Skip to content

Commit

Permalink
[feature] Added dashboard charts: geo, model, os, hardware
Browse files Browse the repository at this point in the history
  • Loading branch information
nemesifier committed Mar 19, 2021
1 parent 3cc3be3 commit d81015b
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 50 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,20 @@ jobs:
sudo apt update
sudo apt-get -qq -y install sqlite3 gdal-bin libproj-dev libgeos-dev libspatialite-dev spatialite-bin libsqlite3-mod-spatialite
- name: Install python dependencies
run: |
pip install -U "pip~=20.2" wheel setuptools
pip install -U -r requirements-test.txt
- name: Install npm dependencies
run: sudo npm install -g jshint stylelint

- name: Upgrade python system packages
run: pip install -U "pip==20.2.4" wheel setuptools

- name: Install openwisp-controller
run: |
pip install -e .
- name: Install test dependencies
run: |
pip install ${{ matrix.django-version }}
./install-dev.sh
pip install -U -r requirements-test.txt
- name: QA checks
run: |
Expand Down
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ RUN apt update && \
sqlite3 libsqlite3-dev openssl libssl-dev && \
rm -rf /var/lib/apt/lists/* /root/.cache/pip/* /tmp/*

COPY install-dev.sh requirements-test.txt requirements.txt /opt/openwisp/
COPY requirements-test.txt requirements.txt /opt/openwisp/
RUN pip install -r /opt/openwisp/requirements.txt && \
pip install -r /opt/openwisp/requirements-test.txt && \
pip install redis && \
bash /opt/openwisp/install-dev.sh && \
rm -rf /var/lib/apt/lists/* /root/.cache/pip/* /tmp/*

ADD . /opt/openwisp
Expand Down
3 changes: 1 addition & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -626,13 +626,12 @@ Install your forked repo:
git clone git://github.com/<your_fork>/openwisp-controller
cd openwisp-controller/
python setup.py develop
pip install -e .
Install development dependencies:

.. code-block:: shell
./install-dev.sh
pip install -r requirements-test.txt
npm install -g jslint
Expand Down
5 changes: 0 additions & 5 deletions install-dev.sh

This file was deleted.

68 changes: 61 additions & 7 deletions openwisp_controller/config/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
)
from swapper import get_model_name, load_model

from openwisp_utils.admin_theme import register_dashboard_element
from openwisp_utils.admin_theme import register_dashboard_chart

from . import settings as app_settings
from .signals import config_modified
Expand All @@ -31,7 +31,7 @@ def ready(self, *args, **kwargs):
self.register_notification_types()
self.add_ignore_notification_widget()
self.enable_cache_invalidation()
self.register_dashboard_element()
self.register_dashboard_charts()

def __setmodels__(self):
self.device_model = load_model('config', 'Device')
Expand Down Expand Up @@ -165,16 +165,70 @@ def enable_cache_invalidation(self):
dispatch_uid='invalidate_checksum_cache',
)

def register_dashboard_element(self):
register_dashboard_element(
def register_dashboard_charts(self):
register_dashboard_chart(
position=1,
element_config={
'name': 'Configuration Status',
config={
'name': _('Configuration Status'),
'query_params': {
'app_label': 'config',
'model': 'device',
'group_by': 'config__status',
},
'colors': {'applied': 'green', 'modified': 'orange', 'error': 'red'},
'colors': {
'applied': '#267126',
'modified': '#ffb442',
'error': '#a72d1d',
},
'labels': {
'applied': _('applied'),
'modified': _('modified'),
'error': _('error'),
},
},
)
register_dashboard_chart(
position=10,
config={
'name': _('Device Models'),
'query_params': {
'app_label': 'config',
'model': 'device',
'group_by': 'model',
},
# since the field can be empty, we need to
# define a label and a color for the empty case
'colors': {'': '#353c44'},
'labels': {'': _('undefined')},
},
)
register_dashboard_chart(
position=11,
config={
'name': _('Firmware version'),
'query_params': {
'app_label': 'config',
'model': 'device',
'group_by': 'os',
},
# since the field can be empty, we need to
# define a label and a color for the empty case
'colors': {'': '#353c44'},
'labels': {'': _('undefined')},
},
)
register_dashboard_chart(
position=12,
config={
'name': _('System type'),
'query_params': {
'app_label': 'config',
'model': 'device',
'group_by': 'system',
},
# since the field can be empty, we need to
# define a label and a color for the empty case
'colors': {'': '#353c44'},
'labels': {'': _('undefined')},
},
)
19 changes: 0 additions & 19 deletions openwisp_controller/config/tests/test_app.py

This file was deleted.

53 changes: 53 additions & 0 deletions openwisp_controller/config/tests/test_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from django.test import TestCase

from openwisp_utils.admin_theme.dashboard import DASHBOARD_CHARTS


class TestApps(TestCase):
def test_config_status_chart_registered(self):
expected_config = {
'name': 'Configuration Status',
'query_params': {
'app_label': 'config',
'model': 'device',
'group_by': 'config__status',
},
'colors': {'applied': '#267126', 'modified': '#ffb442', 'error': '#a72d1d'},
'labels': {'applied': 'applied', 'error': 'error', 'modified': 'modified'},
}
chart_config = DASHBOARD_CHARTS.get(1, None)
self.assertIsNotNone(chart_config)
self.assertDictEqual(chart_config, expected_config)

def test_device_models_chart_registered(self):
chart_config = DASHBOARD_CHARTS.get(10, None)
self.assertIsNotNone(chart_config)
self.assertEqual(chart_config['name'], 'Device Models')
self.assertIn('labels', chart_config)
self.assertDictEqual(chart_config['labels'], {'': 'undefined'})
self.assertNotIn('filters', chart_config)
query_params = chart_config['query_params']
self.assertIn('group_by', query_params)
self.assertEqual(query_params['group_by'], 'model')

def test_firmware_version_chart_registered(self):
chart_config = DASHBOARD_CHARTS.get(11, None)
self.assertIsNotNone(chart_config)
self.assertEqual(chart_config['name'], 'Firmware version')
self.assertIn('labels', chart_config)
self.assertDictEqual(chart_config['labels'], {'': 'undefined'})
self.assertNotIn('filters', chart_config)
query_params = chart_config['query_params']
self.assertIn('group_by', query_params)
self.assertEqual(query_params['group_by'], 'os')

def test_system_type_chart_registered(self):
chart_config = DASHBOARD_CHARTS.get(12, None)
self.assertIsNotNone(chart_config)
self.assertEqual(chart_config['name'], 'System type')
self.assertIn('labels', chart_config)
self.assertDictEqual(chart_config['labels'], {'': 'undefined'})
self.assertNotIn('filters', chart_config)
query_params = chart_config['query_params']
self.assertIn('group_by', query_params)
self.assertEqual(query_params['group_by'], 'system')
2 changes: 1 addition & 1 deletion openwisp_controller/config/tests/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_device_registered(self):
self.assertIn('The existing device', notification.message)

@patch('openwisp_notifications.types.NOTIFICATION_TYPES', {})
@patch('openwisp_utils.admin_theme.dashboard.DASHBOARD_CONFIG', {})
@patch('openwisp_utils.admin_theme.dashboard.DASHBOARD_CHARTS', {})
def test_default_notification_type_already_unregistered(self):
# Simulates if 'default notification type is already unregistered
# by some other module
Expand Down
17 changes: 17 additions & 0 deletions openwisp_controller/geo/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,22 @@ class DeviceLocationInline(ObjectLocationMixin, admin.StackedInline):
admin.site.register(Location, LocationAdmin)


class DeviceLocationFilter(admin.SimpleListFilter):
title = _('has geographic position set?')
parameter_name = 'with_geo'

def lookups(self, request, model_admin):
return (
('true', _('Yes')),
('false', _('No')),
)

def queryset(self, request, queryset):
if self.value():
return queryset.filter(devicelocation__isnull=self.value() == 'false')
return queryset


# Prepend DeviceLocationInline to config.DeviceAdmin
DeviceAdmin.inlines.insert(1, DeviceLocationInline)
DeviceAdmin.list_filter.append(DeviceLocationFilter)
38 changes: 38 additions & 0 deletions openwisp_controller/geo/apps.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import swapper
from django.conf import settings
from django.db.models import Case, Count, Sum, When
from django.utils.translation import ugettext_lazy as _
from django_loci.apps import LociConfig

from openwisp_utils.admin_theme import register_dashboard_chart


class GeoConfig(LociConfig):
name = 'openwisp_controller.geo'
Expand All @@ -14,6 +17,7 @@ def __setmodels__(self):

def ready(self):
super().ready()
self.register_dashboard_charts()
if getattr(settings, 'TESTING', False):
self._add_params_to_test_config()

Expand All @@ -40,3 +44,37 @@ def _add_params_to_test_config(self):
for key in delete_keys:
del params[key]
TestConfigAdmin._additional_params.update(params)

def register_dashboard_charts(self):
register_dashboard_chart(
position=2,
config={
'name': _('Geographic positioning'),
'query_params': {
'app_label': 'config',
'model': 'device',
'annotate': {
'with_geo': Count(
Case(When(devicelocation__isnull=False, then=1,))
),
'without_geo': Count(
Case(When(devicelocation__isnull=True, then=1,))
),
},
'aggregate': {
'with_geo__sum': Sum('with_geo'),
'without_geo__sum': Sum('without_geo'),
},
},
'colors': {'with_geo__sum': '#267126', 'without_geo__sum': '#353c44'},
'labels': {
'with_geo__sum': _('With geographic position'),
'without_geo__sum': _('Without geographic position'),
},
'filters': {
'key': 'with_geo',
'with_geo__sum': 'true',
'without_geo__sum': 'false',
},
},
)
19 changes: 19 additions & 0 deletions openwisp_controller/geo/tests/test_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.test import TestCase

from openwisp_utils.admin_theme.dashboard import DASHBOARD_CHARTS


class TestApps(TestCase):
def test_geo_chart_registered(self):
chart_config = DASHBOARD_CHARTS.get(2, None)
self.assertIsNotNone(chart_config)
self.assertEqual(chart_config['name'], 'Geographic positioning')
self.assertIn('labels', chart_config)
query_params = chart_config['query_params']
self.assertIn('annotate', query_params)
self.assertIn('aggregate', query_params)
self.assertIn('filters', chart_config)
filters = chart_config['filters']
self.assertIn('key', filters)
self.assertIn('with_geo__sum', chart_config['filters'])
self.assertIn('without_geo__sum', chart_config['filters'])
2 changes: 1 addition & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openwisp-utils[qa]
redis>=3.4.1,<4.0.0
channels_redis~=3.1
django_redis~=4.12
openwisp-utils[qa]~=0.7.3
pytest~=6.0
pytest-django>=3.8.0,<4.0.0
pytest-asyncio~=0.14.0
Expand Down
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ django-x509~=0.9.2
django-taggit~=1.3.0
django-loci~=0.4.0
django-flat-json-widget~=0.1.2
# TODO: change this when next version of openwisp_users is released
# TODO: change this when next point version of openwisp-utils is released
openwisp-utils[rest] @ https://github.com/openwisp/openwisp-utils/tarball/master
# TODO: change this when next point version of openwisp-users is released
openwisp-users @ https://github.com/openwisp/openwisp-users/tarball/master
openwisp-utils[rest]~=0.7.1
openwisp-notifications~=0.3
djangorestframework-gis>=0.12.0,<0.17.0
netjsonconfig~=0.9.0
Expand Down
8 changes: 3 additions & 5 deletions tests/openwisp2/sample_config/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from openwisp_controller.config.tests.test_admin import TestAdmin as BaseTestAdmin
from openwisp_controller.config.tests.test_app import (
TestCustomAdminDashboard as BaseTestCustomAdminDashboard,
)
from openwisp_controller.config.tests.test_apps import TestApps as BaseTestApps
from openwisp_controller.config.tests.test_config import TestConfig as BaseTestConfig
from openwisp_controller.config.tests.test_controller import (
TestController as BaseTestController,
Expand Down Expand Up @@ -68,7 +66,7 @@ class TestVpnTransaction(BaseTestVpnTransaction):
pass


class TestCustomAdminDashboard(BaseTestCustomAdminDashboard):
class TestApps(BaseTestApps):
pass


Expand All @@ -83,4 +81,4 @@ class TestCustomAdminDashboard(BaseTestCustomAdminDashboard):
del BaseTestViews
del BaseTestVpn
del BaseTestVpnTransaction
del BaseTestCustomAdminDashboard
del BaseTestApps
Loading

0 comments on commit d81015b

Please sign in to comment.