diff --git a/openwisp_monitoring/check/base/models.py b/openwisp_monitoring/check/base/models.py index c5156698..089717ad 100644 --- a/openwisp_monitoring/check/base/models.py +++ b/openwisp_monitoring/check/base/models.py @@ -93,6 +93,9 @@ def check_instance(self): def perform_check(self, store=True): """Initializes check instance and calls the check method.""" if ( + hasattr(self.content_object, 'is_deactivated') + and self.content_object.is_deactivated() + ) or ( hasattr(self.content_object, 'organization_id') and self.content_object.organization.is_active is False ): diff --git a/openwisp_monitoring/check/tests/test_models.py b/openwisp_monitoring/check/tests/test_models.py index 503a5805..e9b9d102 100644 --- a/openwisp_monitoring/check/tests/test_models.py +++ b/openwisp_monitoring/check/tests/test_models.py @@ -290,6 +290,16 @@ def test_device_organization_disabled_check_not_performed(self): check.perform_check() mocked_check.assert_not_called() + def test_deactivated_device_check_not_performed(self): + config = self._create_config(status='modified', organization=self._create_org()) + self.assertEqual(Check.objects.filter(is_active=True).count(), 3) + config.device.deactivate() + config.set_status_deactivated() + check = Check.objects.filter(check_type=self._CONFIG_APPLIED).first() + with patch(f'{self._CONFIG_APPLIED}.check') as mocked_check: + check.perform_check() + mocked_check.assert_not_called() + def test_config_check_problem_with_interval(self): self._create_admin() d = self._create_device(organization=self._create_org()) diff --git a/openwisp_monitoring/device/admin.py b/openwisp_monitoring/device/admin.py index f35aa00c..ba0e973b 100644 --- a/openwisp_monitoring/device/admin.py +++ b/openwisp_monitoring/device/admin.py @@ -21,8 +21,8 @@ ) from swapper import load_model -from openwisp_controller.config.admin import DeviceAdmin as BaseDeviceAdmin from openwisp_controller.config.admin import DeactivatedDeviceReadOnlyMixin +from openwisp_controller.config.admin import DeviceAdmin as BaseDeviceAdmin from openwisp_users.multitenancy import MultitenantAdminMixin from openwisp_utils.admin import ReadOnlyAdmin @@ -59,32 +59,68 @@ def full_clean(self): class InlinePermissionMixin: + """ + Manages permissions for the inline objects. + + This mixin class handles the permissions for the inline objects. + It checks if the user has permission to modify the main object + (e.g., view_model, add_model, change_model, or delete_model), + and if not, it checks if the user has permission to modify the + inline object (e.g., view_model_inline, add_model_inline, + change_model_inline, or delete_model_inline). + """ + def has_add_permission(self, request, obj=None): - # User will be able to add objects from inline even - # if it only has permission to add a model object - return super().has_add_permission(request, obj) and request.user.has_perm( + return super(admin.options.InlineModelAdmin, self).has_add_permission( + request + ) or request.user.has_perm( f'{self.model._meta.app_label}.add_{self.inline_permission_suffix}' ) def has_change_permission(self, request, obj=None): - return super().has_change_permission(request, obj) and request.user.has_perm( + return super(admin.options.InlineModelAdmin, self).has_change_permission( + request, obj + ) or request.user.has_perm( f'{self.model._meta.app_label}.change_{self.inline_permission_suffix}' ) def has_view_permission(self, request, obj=None): - return super().has_view_permission(request, obj) and request.user.has_perm( + return super(admin.options.InlineModelAdmin, self).has_view_permission( + request, obj + ) or request.user.has_perm( f'{self.model._meta.app_label}.view_{self.inline_permission_suffix}' ) def has_delete_permission(self, request, obj=None): - return super().has_delete_permission(request, obj) and request.user.has_perm( + return super(admin.options.InlineModelAdmin, self).has_delete_permission( + request, obj + ) or request.user.has_perm( f'{self.model._meta.app_label}.delete_{self.inline_permission_suffix}' ) -class CheckInline( - InlinePermissionMixin, DeactivatedDeviceReadOnlyMixin, GenericStackedInline +class DeactivatedDeviceReadOnlyInlinePermissionMixin( + DeactivatedDeviceReadOnlyMixin, + InlinePermissionMixin, ): + def has_add_permission(self, request, obj=None): + perm = super().has_add_permission(request, obj) + return self._has_permission(request, obj, perm) + + def has_change_permission(self, request, obj=None): + perm = super().has_change_permission(request, obj) + return self._has_permission(request, obj, perm) + + def has_view_permission(self, request, obj=None): + perm = super().has_view_permission(request, obj) + return self._has_permission(request, obj, perm) + + def has_delete_permission(self, request, obj=None): + perm = super().has_delete_permission(request, obj) + return self._has_permission(request, obj, perm) + + +class CheckInline(DeactivatedDeviceReadOnlyInlinePermissionMixin, GenericStackedInline): model = Check extra = 0 formset = CheckInlineFormSet @@ -156,7 +192,7 @@ def get_queryset(self, request): class MetricInline( - InlinePermissionMixin, DeactivatedDeviceReadOnlyMixin, NestedGenericStackedInline + DeactivatedDeviceReadOnlyInlinePermissionMixin, NestedGenericStackedInline ): model = Metric extra = 0 diff --git a/openwisp_monitoring/device/api/views.py b/openwisp_monitoring/device/api/views.py index 2f0d7390..aa9f22f1 100644 --- a/openwisp_monitoring/device/api/views.py +++ b/openwisp_monitoring/device/api/views.py @@ -111,7 +111,7 @@ class DeviceMetricView( model = DeviceData queryset = ( - DeviceData.objects.filter(organization__is_active=True) + DeviceData.objects.filter(organization__is_active=True, _is_deactivated=False) .only( 'id', 'key', diff --git a/openwisp_monitoring/device/apps.py b/openwisp_monitoring/device/apps.py index 67d98807..bf13fd4a 100644 --- a/openwisp_monitoring/device/apps.py +++ b/openwisp_monitoring/device/apps.py @@ -9,7 +9,11 @@ from django.utils.translation import gettext_lazy as _ from swapper import get_model_name, load_model -from openwisp_controller.config.signals import checksum_requested, config_status_changed +from openwisp_controller.config.signals import ( + checksum_requested, + config_status_changed, + device_deactivated, +) from openwisp_controller.connection import settings as connection_settings from openwisp_controller.connection.signals import is_working_changed from openwisp_utils.admin_theme import ( @@ -74,6 +78,7 @@ def connect_device_signals(self): Device = load_model('config', 'Device') DeviceData = load_model('device_monitoring', 'DeviceData') + DeviceMonitoring = load_model('device_monitoring', 'DeviceMonitoring') DeviceLocation = load_model('geo', 'DeviceLocation') Metric = load_model('monitoring', 'Metric') Chart = load_model('monitoring', 'Chart') @@ -145,6 +150,11 @@ def connect_device_signals(self): sender=Organization, dispatch_uid='post_save_organization_disabled_monitoring', ) + device_deactivated.connect( + DeviceMonitoring.handle_deactivated_device, + sender=Device, + dispatch_uid='device_deactivated_update_devicemonitoring', + ) @classmethod def device_post_save_receiver(cls, instance, created, **kwargs): diff --git a/openwisp_monitoring/device/base/models.py b/openwisp_monitoring/device/base/models.py index 60a72f3c..8d6c5630 100644 --- a/openwisp_monitoring/device/base/models.py +++ b/openwisp_monitoring/device/base/models.py @@ -444,6 +444,19 @@ def handle_disabled_organization(cls, organization_id): status='unknown' ) + @classmethod + def handle_deactivated_device(cls, instance, **kwargs): + """Handles the deactivation of a device + + Sets the device's monitoring status to 'deactivated' + + Parameters: - instance (Device): The device object + which is deactivated + + Returns: - None + """ + cls.objects.filter(device_id=instance.id).update(status='deactivated') + @classmethod def _get_critical_metric_keys(cls): return [metric['key'] for metric in get_critical_device_metrics()] diff --git a/openwisp_monitoring/device/static/monitoring/css/device-map.css b/openwisp_monitoring/device/static/monitoring/css/device-map.css index af7ced50..1d79b7a1 100644 --- a/openwisp_monitoring/device/static/monitoring/css/device-map.css +++ b/openwisp_monitoring/device/static/monitoring/css/device-map.css @@ -1,4 +1,5 @@ -.health-unknown { +.health-unknown, +.health-deactivated { color: grey; } .health-ok { diff --git a/openwisp_monitoring/device/static/monitoring/css/monitoring.css b/openwisp_monitoring/device/static/monitoring/css/monitoring.css index fb58363d..d595d99a 100644 --- a/openwisp_monitoring/device/static/monitoring/css/monitoring.css +++ b/openwisp_monitoring/device/static/monitoring/css/monitoring.css @@ -38,7 +38,8 @@ td.field-health_status, font-weight: bold; text-transform: uppercase; } -.health-unknown { +.health-unknown, +.health-deactivated { color: grey; } .health-ok { diff --git a/openwisp_monitoring/device/tests/test_api.py b/openwisp_monitoring/device/tests/test_api.py index 9a56d314..792d1ce7 100644 --- a/openwisp_monitoring/device/tests/test_api.py +++ b/openwisp_monitoring/device/tests/test_api.py @@ -348,6 +348,15 @@ def test_404_disabled_organization(self): self.assertEqual(self.metric_queryset.count(), 0) self.assertEqual(self.chart_queryset.count(), 0) + def test_404_deactivated_device(self): + device = self._create_device() + device.deactivate() + with self.assertNumQueries(2): + response = self._post_data(device.id, device.key, self._data()) + self.assertEqual(response.status_code, 404) + self.assertEqual(self.metric_queryset.count(), 0) + self.assertEqual(self.chart_queryset.count(), 0) + def test_garbage_wireless_clients(self): o = self._create_org() d = self._create_device(organization=o) diff --git a/openwisp_monitoring/device/tests/test_models.py b/openwisp_monitoring/device/tests/test_models.py index 412da9ca..6ec4430b 100644 --- a/openwisp_monitoring/device/tests/test_models.py +++ b/openwisp_monitoring/device/tests/test_models.py @@ -797,6 +797,15 @@ def test_handle_disabled_organization(self): self.assertEqual(device_monitoring.status, 'unknown') self.assertEqual(device.management_ip, None) + def test_handle_deactivated_device(self): + device_monitoring, _, _, _ = self._create_env() + device = device_monitoring.device + self.assertEqual(device_monitoring.status, 'ok') + device.deactivate() + device_monitoring.refresh_from_db() + device.refresh_from_db() + self.assertEqual(device_monitoring.status, 'deactivated') + class TestWifiClientSession(TestWifiClientSessionMixin, TestCase): wifi_client_model = WifiClient