• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openwisp / openwisp-monitoring / 21801692450

08 Feb 2026 04:44PM UTC coverage: 98.429% (-0.1%) from 98.561%
21801692450

Pull #738

github

web-flow
Merge 4e08f7c01 into 331fca2bf
Pull Request #738: [gsoc25] Add indoor maps, enhanced dashboard map UI, shareable URLs and real-time device position updates

3885 of 3947 relevant lines covered (98.43%)

10.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.09
openwisp_monitoring/device/admin.py
1
import json
11✔
2
import uuid
11✔
3
from urllib.parse import urljoin
11✔
4

5
from django import forms
11✔
6
from django.contrib import admin
11✔
7
from django.contrib.contenttypes.admin import GenericStackedInline
11✔
8
from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
11✔
9
from django.contrib.contenttypes.models import ContentType
11✔
10
from django.forms import ModelForm
11✔
11
from django.templatetags.static import static
11✔
12
from django.urls import resolve, reverse, reverse_lazy
11✔
13
from django.utils import timezone
11✔
14
from django.utils.formats import localize
11✔
15
from django.utils.html import format_html
11✔
16
from django.utils.safestring import mark_safe
11✔
17
from django.utils.translation import gettext_lazy as _
11✔
18
from import_export.admin import ImportExportMixin
11✔
19
from import_export.forms import ExportForm
11✔
20
from nested_admin.nested import (
11✔
21
    NestedGenericStackedInline,
22
    NestedModelAdmin,
23
    NestedStackedInline,
24
)
25
from swapper import load_model
11✔
26

27
from openwisp_controller.config.admin import DeactivatedDeviceReadOnlyMixin
11✔
28
from openwisp_controller.config.admin import DeviceAdmin as BaseDeviceAdmin
11✔
29
from openwisp_controller.geo.admin import DeviceLocationInline
11✔
30
from openwisp_users.multitenancy import MultitenantAdminMixin
11✔
31
from openwisp_utils.admin import ReadOnlyAdmin
11✔
32

33
from ..monitoring.admin import MetricAdmin
11✔
34
from ..settings import MONITORING_API_BASEURL, MONITORING_API_URLCONF
11✔
35
from . import settings as app_settings
11✔
36
from .exportable import DeviceMonitoringResource
11✔
37
from .filters import DeviceFilter, DeviceGroupFilter, DeviceOrganizationFilter
11✔
38

39
DeviceData = load_model("device_monitoring", "DeviceData")
11✔
40
WifiSession = load_model("device_monitoring", "WifiSession")
11✔
41
DeviceMonitoring = load_model("device_monitoring", "DeviceMonitoring")
11✔
42
AlertSettings = load_model("monitoring", "AlertSettings")
11✔
43
Chart = load_model("monitoring", "Chart")
11✔
44
Device = load_model("config", "Device")
11✔
45
Metric = load_model("monitoring", "Metric")
11✔
46
Notification = load_model("openwisp_notifications", "Notification")
11✔
47
Check = load_model("check", "Check")
11✔
48
Organization = load_model("openwisp_users", "Organization")
11✔
49
MapPage = load_model("device_monitoring", "Map")
11✔
50

51

52
class CheckInlineFormSet(BaseGenericInlineFormSet):
11✔
53
    def full_clean(self):
11✔
54
        for form in self.forms:
11✔
55
            obj = form.instance
11✔
56
            if not obj.content_type or not obj.object_id:
11✔
57
                setattr(
11✔
58
                    obj,
59
                    self.ct_field.get_attname(),
60
                    ContentType.objects.get_for_model(self.instance).pk,
61
                )
62
                setattr(obj, self.ct_fk_field.get_attname(), self.instance.pk)
11✔
63
        super().full_clean()
11✔
64

65

66
class InlinePermissionMixin:
11✔
67
    """
68
    Manages permissions for the inline objects.
69

70
    This mixin class handles the permissions for the inline objects.
71
    It checks if the user has permission to modify the main object
72
    (e.g., view_model, add_model, change_model, or delete_model),
73
    and if not, it checks if the user has permission to modify the
74
    inline object (e.g., view_model_inline, add_model_inline,
75
    change_model_inline, or delete_model_inline).
76
    """
77

78
    def has_add_permission(self, request, obj=None):
11✔
79
        return super(admin.options.InlineModelAdmin, self).has_add_permission(
11✔
80
            request
81
        ) or request.user.has_perm(
82
            f"{self.model._meta.app_label}.add_{self.inline_permission_suffix}"
83
        )
84

85
    def has_change_permission(self, request, obj=None):
11✔
86
        return super(admin.options.InlineModelAdmin, self).has_change_permission(
11✔
87
            request, obj
88
        ) or request.user.has_perm(
89
            f"{self.model._meta.app_label}.change_{self.inline_permission_suffix}"
90
        )
91

92
    def has_view_permission(self, request, obj=None):
11✔
93
        return super(admin.options.InlineModelAdmin, self).has_view_permission(
11✔
94
            request, obj
95
        ) or request.user.has_perm(
96
            f"{self.model._meta.app_label}.view_{self.inline_permission_suffix}"
97
        )
98

99
    def has_delete_permission(self, request, obj=None):
11✔
100
        return super(admin.options.InlineModelAdmin, self).has_delete_permission(
11✔
101
            request, obj
102
        ) or request.user.has_perm(
103
            f"{self.model._meta.app_label}.delete_{self.inline_permission_suffix}"
104
        )
105

106

107
class DeactivatedDeviceReadOnlyInlinePermissionMixin(
11✔
108
    DeactivatedDeviceReadOnlyMixin,
109
    InlinePermissionMixin,
110
):
111
    def has_add_permission(self, request, obj=None):
11✔
112
        perm = super().has_add_permission(request, obj)
11✔
113
        return self._has_permission(request, obj, perm)
11✔
114

115
    def has_change_permission(self, request, obj=None):
11✔
116
        perm = super().has_change_permission(request, obj)
11✔
117
        return self._has_permission(request, obj, perm)
11✔
118

119
    def has_view_permission(self, request, obj=None):
11✔
120
        perm = super().has_view_permission(request, obj)
11✔
121
        return self._has_permission(request, obj, perm)
11✔
122

123
    def has_delete_permission(self, request, obj=None):
11✔
124
        perm = super().has_delete_permission(request, obj)
11✔
125
        return self._has_permission(request, obj, perm)
11✔
126

127

128
class CheckInline(DeactivatedDeviceReadOnlyInlinePermissionMixin, GenericStackedInline):
11✔
129
    model = Check
11✔
130
    extra = 0
11✔
131
    formset = CheckInlineFormSet
11✔
132
    fields = [
11✔
133
        "is_active",
134
        "check_type",
135
    ]
136
    inline_permission_suffix = "check_inline"
11✔
137

138
    def get_fields(self, request, obj=None):
11✔
139
        if not self.has_change_permission(request, obj) or not self.has_view_permission(
11✔
140
            request, obj
141
        ):
142
            return ["check_type", "is_active"]
11✔
143
        return super().get_fields(request, obj)
11✔
144

145
    def get_readonly_fields(self, request, obj=None):
11✔
146
        if not self.has_change_permission(request, obj) or not self.has_view_permission(
11✔
147
            request, obj
148
        ):
149
            return ["check_type"]
11✔
150
        return super().get_readonly_fields(request, obj)
11✔
151

152

153
class AlertSettingsForm(ModelForm):
11✔
154
    def __init__(self, *args, **kwargs):
11✔
155
        instance = kwargs.get("instance")
11✔
156
        if instance:
11✔
157
            kwargs["initial"] = {
11✔
158
                "custom_tolerance": instance.tolerance,
159
                "custom_operator": instance.operator,
160
                "custom_threshold": instance.threshold,
161
            }
162
        super().__init__(*args, **kwargs)
11✔
163

164
    def _post_clean(self):
11✔
165
        self.instance._delete_instance = False
11✔
166
        if all(
11✔
167
            self.cleaned_data[field] is None
168
            for field in [
169
                "custom_operator",
170
                "custom_threshold",
171
                "custom_tolerance",
172
            ]
173
        ):
174
            # "_delete_instance" flag signifies that
175
            # the fields have been set to None by the
176
            # user. Hence, the object should be deleted.
177
            self.instance._delete_instance = True
11✔
178
        super()._post_clean()
11✔
179

180
    def save(self, commit=True):
11✔
181
        if self.instance._delete_instance:
11✔
182
            self.instance.delete()
11✔
183
            return self.instance
11✔
184
        return super().save(commit)
11✔
185

186

187
class AlertSettingsInline(InlinePermissionMixin, NestedStackedInline):
11✔
188
    model = AlertSettings
11✔
189
    extra = 1
11✔
190
    max_num = 1
11✔
191
    exclude = ["created", "modified"]
11✔
192
    form = AlertSettingsForm
11✔
193
    inline_permission_suffix = "alertsettings_inline"
11✔
194

195
    def get_queryset(self, request):
11✔
196
        return super().get_queryset(request).order_by("created")
11✔
197

198

199
class MetricInline(
11✔
200
    DeactivatedDeviceReadOnlyInlinePermissionMixin, NestedGenericStackedInline
201
):
202
    model = Metric
11✔
203
    extra = 0
11✔
204
    inlines = [AlertSettingsInline]
11✔
205
    fieldsets = [
11✔
206
        (
207
            None,
208
            {
209
                "fields": (
210
                    "name",
211
                    "is_healthy",
212
                )
213
            },
214
        ),
215
        (
216
            _("Advanced options"),
217
            {"classes": ("collapse",), "fields": ("field_name",)},
218
        ),
219
    ]
220

221
    readonly_fields = ["name", "is_healthy"]
11✔
222
    # Explicitly changed name from Metrics to Alert Settings
223
    verbose_name = _("Alert Settings")
11✔
224
    verbose_name_plural = verbose_name
11✔
225
    inline_permission_suffix = "alertsettings_inline"
11✔
226
    # Ordering queryset by metric name
227
    ordering = ("name",)
11✔
228

229
    def get_fieldsets(self, request, obj=None):
11✔
230
        if not self.has_change_permission(request, obj) or not self.has_view_permission(
11✔
231
            request, obj
232
        ):
233
            return [
11✔
234
                (None, {"fields": ("is_healthy",)}),
235
            ]
236
        return super().get_fieldsets(request, obj)
11✔
237

238
    def get_queryset(self, request):
11✔
239
        # Only show 'Metrics' that have 'AlertSettings' objects
240
        return super().get_queryset(request).filter(alertsettings__isnull=False)
11✔
241

242
    def has_add_permission(self, request, obj=None):
11✔
243
        # We need to restrict the users from adding the 'metrics' since
244
        # they're created by the system automatically with default 'alertsettings'
245
        return False
11✔
246

247
    def has_delete_permission(self, request, obj=None):
11✔
248
        # We need to restrict the users from deleting the 'metrics' since
249
        # they're created by the system automatically with default 'alertsettings'
250
        return False
11✔
251

252

253
class DeviceAdmin(BaseDeviceAdmin, NestedModelAdmin):
11✔
254
    change_form_template = "admin/monitoring/device/change_form.html"
11✔
255
    list_filter = ["monitoring__status"] + BaseDeviceAdmin.list_filter
11✔
256
    list_select_related = ["monitoring"] + list(BaseDeviceAdmin.list_select_related)
11✔
257
    list_display = list(BaseDeviceAdmin.list_display)
11✔
258
    list_display.insert(list_display.index("config_status"), "health_status")
11✔
259
    readonly_fields = ["health_status"] + BaseDeviceAdmin.readonly_fields
11✔
260

261
    class Media:
11✔
262
        js = (
11✔
263
            tuple(BaseDeviceAdmin.Media.js)
264
            + (
265
                "monitoring/js/lib/percircle.min.js",
266
                "monitoring/js/alert-settings.js",
267
            )
268
            + MetricAdmin.Media.js
269
            + ("monitoring/js/chart-utils.js",)
270
            + ("monitoring/js/lib/moment.min.js",)
271
            + ("monitoring/js/lib/daterangepicker.min.js",)
272
        )
273
        css = {
11✔
274
            "all": (
275
                "monitoring/css/percircle.min.css",
276
                "monitoring/css/daterangepicker.css",
277
            )
278
            + MetricAdmin.Media.css["all"]
279
        }
280

281
    def get_extra_context(self, pk=None):
11✔
282
        ctx = super().get_extra_context(pk)
11✔
283
        if pk:
11✔
284
            device_data = DeviceData(pk=uuid.UUID(pk))
11✔
285
            api_url = reverse(
11✔
286
                "monitoring:api_device_metric",
287
                urlconf=MONITORING_API_URLCONF,
288
                args=[pk],
289
            )
290
            if MONITORING_API_BASEURL:
11✔
291
                api_url = urljoin(MONITORING_API_BASEURL, api_url)
11✔
292
            ctx.update(
11✔
293
                {
294
                    "device_data": device_data.data_user_friendly,
295
                    "api_url": api_url,
296
                    "default_time": Chart.DEFAULT_TIME,
297
                    "MAC_VENDOR_DETECTION": app_settings.MAC_VENDOR_DETECTION,
298
                }
299
            )
300
        return ctx
11✔
301

302
    def health_checks(self, obj):
11✔
303
        metric_rows = []
11✔
304
        for metric in DeviceData(pk=obj.pk).metrics.filter(alertsettings__isnull=False):
11✔
305
            health = "yes" if metric.is_healthy else "no"
11✔
306
            icon_url = static(f"admin/img/icon-{health}.svg")
11✔
307
            metric_rows.append(
11✔
308
                f'<li><img src="{icon_url}" ' f'alt="health"> {metric.name}</li>'
309
            )
310
        return format_html(
11✔
311
            mark_safe(f'<ul class="health_checks">{"".join(metric_rows)}</ul>')
312
        )
313

314
    health_checks.short_description = _("health checks")
11✔
315

316
    def health_status(self, obj):
11✔
317
        return format_html(
11✔
318
            mark_safe('<span class="health-{0}">{1}</span>'),
319
            obj.monitoring.status,
320
            obj.monitoring.get_status_display(),
321
        )
322

323
    health_status.short_description = _("health status")
11✔
324

325
    def get_object(self, request, object_id, from_field=None):
11✔
326
        obj = super().get_object(request, object_id, from_field=from_field)
11✔
327
        if obj and obj.wifisession_set.exists():
11✔
328
            # We need to provide default formset values
329
            # to avoid management formset errors when wifi sessions
330
            # are created while editing the DeviceAdmin change page
331
            wifisession_formset_data = {
11✔
332
                "wifisession_set-TOTAL_FORMS": "1",
333
                "wifisession_set-INITIAL_FORMS": "1",
334
            }
335
            request.POST = request.POST.copy()
11✔
336
            request.POST.update(wifisession_formset_data)
11✔
337
        return obj
11✔
338

339
    def get_form(self, request, obj=None, **kwargs):
11✔
340
        """Adds the help_text of DeviceMonitoring.status field"""
341
        health_status = DeviceMonitoring._meta.get_field("status").help_text
11✔
342
        kwargs.update(
11✔
343
            {"help_texts": {"health_status": health_status.replace("\n", "<br>")}}
344
        )
345
        return super().get_form(request, obj, **kwargs)
11✔
346

347
    def get_fields(self, request, obj=None):
11✔
348
        fields = list(super().get_fields(request, obj))
11✔
349
        if obj and not obj._state.adding:
11✔
350
            fields.insert(fields.index("last_ip"), "health_status")
11✔
351
        if (
11✔
352
            not obj
353
            or not hasattr(obj, "monitoring")
354
            or obj.monitoring.status in ["ok", "unknown", "deactivated"]
355
        ):
356
            return fields
11✔
357
        fields.insert(fields.index("health_status") + 1, "health_checks")
11✔
358
        return fields
11✔
359

360
    def get_readonly_fields(self, request, obj=None):
11✔
361
        readonly_fields = super().get_readonly_fields(request, obj)
11✔
362
        if (
11✔
363
            not obj
364
            or not hasattr(obj, "monitoring")
365
            or obj.monitoring.status in ["ok", "unknown"]
366
        ):
367
            return readonly_fields
11✔
368
        readonly_fields = list(readonly_fields)
11✔
369
        readonly_fields.append("health_checks")
11✔
370
        return readonly_fields
11✔
371

372
    def get_inlines(self, request, obj=None):
11✔
373
        inlines = super().get_inlines(request, obj)
11✔
374
        inlines = list(inlines + [CheckInline, MetricInline])
11✔
375
        # This attribute needs to be set for nested inline
376
        for inline in inlines:
11✔
377
            if not hasattr(inline, "sortable_options"):
11✔
378
                inline.sortable_options = {"disabled": True}
11✔
379
        if not obj or obj._state.adding or obj.organization.is_active is False:
11✔
380
            inlines.remove(CheckInline)
11✔
381
            inlines.remove(MetricInline)
11✔
382
        return inlines
11✔
383

384

385
class DeviceAdminExportable(ImportExportMixin, DeviceAdmin):
11✔
386
    export_form_class = ExportForm
11✔
387
    resource_class = DeviceMonitoringResource
11✔
388
    # Added to support both reversion and import-export
389
    change_list_template = "admin/config/change_list_device.html"
11✔
390

391

392
class WifiSessionAdminHelperMixin:
11✔
393
    def _get_boolean_html(self, value):
11✔
394
        icon_type = "unknown"
11✔
395
        if value is True:
11✔
396
            icon_type = "yes"
11✔
397
        elif value is False:
11✔
398
            icon_type = "no"
11✔
399
        icon = static(f"admin/img/icon-{icon_type}.svg")
11✔
400
        return mark_safe(f'<img src="{icon}">')
11✔
401

402
    def he(self, obj):
11✔
403
        return self._get_boolean_html(obj.wifi_client.he)
11✔
404

405
    he.short_description = "WiFi 6 (802.11ax)"
11✔
406

407
    def vht(self, obj):
11✔
408
        return self._get_boolean_html(obj.wifi_client.vht)
11✔
409

410
    vht.short_description = "WiFi 5 (802.11ac)"
11✔
411

412
    def ht(self, obj):
11✔
413
        return self._get_boolean_html(obj.wifi_client.ht)
11✔
414

415
    ht.short_description = "WiFi 4 (802.11n)"
11✔
416

417
    def wmm(self, obj):
11✔
418
        return self._get_boolean_html(obj.wifi_client.wmm)
11✔
419

420
    wmm.short_description = "WMM"
11✔
421

422
    def wds(self, obj):
11✔
423
        return self._get_boolean_html(obj.wifi_client.wds)
11✔
424

425
    wds.short_description = "WDS"
11✔
426

427
    def wps(self, obj):
11✔
428
        return self._get_boolean_html(obj.wifi_client.wps)
11✔
429

430
    wps.short_description = "WPS"
11✔
431

432
    def get_stop_time(self, obj):
11✔
433
        if obj.stop_time is None:
11✔
434
            return mark_safe('<strong style="color:green;">online</strong>')
11✔
435
        return localize(timezone.localtime(obj.stop_time))
11✔
436

437
    get_stop_time.short_description = _("stop time")
11✔
438

439
    def related_device(self, obj):
11✔
440
        app_label = Device._meta.app_label
11✔
441
        url = reverse(f"admin:{app_label}_device_change", args=[obj.device_id])
11✔
442
        return mark_safe(f'<a href="{url}">{obj.device}</a>')
11✔
443

444
    related_device.short_description = _("device")
11✔
445

446
    def related_organization(self, obj):
11✔
447
        app_label = Organization._meta.app_label
11✔
448
        url = reverse(
11✔
449
            f"admin:{app_label}_organization_change", args=[obj.device.organization_id]
450
        )
451
        return mark_safe(f'<a href="{url}">{obj.device.organization}</a>')
11✔
452

453
    related_organization.short_description = _("organization")
11✔
454

455

456
class WiFiSessionInline(WifiSessionAdminHelperMixin, admin.TabularInline):
11✔
457
    model = WifiSession
11✔
458
    fk_name = "device"
11✔
459
    fields = [
11✔
460
        "get_mac_address",
461
        "vendor",
462
        "ssid",
463
        "interface_name",
464
        "he",
465
        "vht",
466
        "ht",
467
        "start_time",
468
        "get_stop_time",
469
    ]
470
    readonly_fields = fields
11✔
471
    can_delete = False
11✔
472
    extra = 0
11✔
473
    template = "admin/monitoring/device/wifisession_tabular.html"
11✔
474

475
    class Media:
11✔
476
        css = {"all": ("monitoring/css/wifi-sessions.css",)}
11✔
477
        js = ["admin/js/jquery.init.js", "monitoring/js/wifi-session-inline.js"]
11✔
478

479
    def has_add_permission(self, request, obj=None):
11✔
480
        return False
11✔
481

482
    def has_change_permission(self, request, obj=None):
11✔
483
        return False
11✔
484

485
    def has_delete_permission(self, request, obj=None):
11✔
486
        return False
11✔
487

488
    def get_queryset(self, request, select_related=True):
11✔
489
        qs = super().get_queryset(request).filter(stop_time__isnull=True)
11✔
490
        resolved = resolve(request.path_info)
11✔
491
        if "object_id" in resolved.kwargs:
11✔
492
            qs = qs.filter(device_id=resolved.kwargs["object_id"])
11✔
493
        if select_related:
11✔
494
            qs = qs.select_related("wifi_client")
11✔
495
        return qs
11✔
496

497
    def _get_conditional_queryset(self, request, obj, select_related=False):
11✔
498
        return self.get_queryset(request, select_related=select_related).exists()
11✔
499

500
    def get_mac_address(self, obj):
11✔
501
        app_label = WifiSession._meta.app_label
11✔
502
        url = reverse(f"admin:{app_label}_wifisession_change", args=[obj.id])
11✔
503
        return mark_safe(f'<a href="{url}">{obj.mac_address}</a>')
11✔
504

505
    get_mac_address.short_description = _("MAC address")
11✔
506

507

508
class WifiSessionAdmin(
11✔
509
    WifiSessionAdminHelperMixin, MultitenantAdminMixin, ReadOnlyAdmin
510
):
511
    multitenant_parent = "device"
11✔
512
    model = WifiSession
11✔
513
    list_display = [
11✔
514
        "mac_address",
515
        "vendor",
516
        "related_organization",
517
        "related_device",
518
        "ssid",
519
        "he",
520
        "vht",
521
        "ht",
522
        "start_time",
523
        "get_stop_time",
524
    ]
525
    fields = [
11✔
526
        "related_organization",
527
        "mac_address",
528
        "vendor",
529
        "related_device",
530
        "ssid",
531
        "interface_name",
532
        "he",
533
        "vht",
534
        "ht",
535
        "wmm",
536
        "wds",
537
        "wps",
538
        "start_time",
539
        "get_stop_time",
540
        "modified",
541
    ]
542
    search_fields = ["wifi_client__mac_address", "device__name", "device__mac_address"]
11✔
543
    list_filter = [
11✔
544
        DeviceOrganizationFilter,
545
        DeviceFilter,
546
        DeviceGroupFilter,
547
        "start_time",
548
        "stop_time",
549
    ]
550

551
    def get_readonly_fields(self, request, obj=None):
11✔
552
        fields = super().get_readonly_fields(request, obj)
11✔
553
        fields += [
11✔
554
            "related_organization",
555
            "mac_address",
556
            "vendor",
557
            "he",
558
            "vht",
559
            "ht",
560
            "wmm",
561
            "wds",
562
            "wps",
563
            "get_stop_time",
564
            "modified",
565
            "related_device",
566
        ]
567
        return fields
11✔
568

569
    def get_queryset(self, request):
11✔
570
        return (
11✔
571
            super()
572
            .get_queryset(request)
573
            .select_related(
574
                "wifi_client", "device", "device__organization", "device__group"
575
            )
576
        )
577

578
    def has_delete_permission(self, request, obj=None):
11✔
579
        return super(admin.ModelAdmin, self).has_delete_permission(request, obj)
11✔
580

581

582
class MapPageAdmin(MultitenantAdminMixin, admin.ModelAdmin):
11✔
583
    """
584
    Overrides the changelist template of proxy Model Map to render
585
    a full-screen interactive map using custom template map_page.html.
586
    """
587

588
    change_list_template = "admin/map/map_page.html"
11✔
589

590
    class Media:
11✔
591
        js = [
11✔
592
            "monitoring/js/lib/netjsongraph.min.js",
593
            "monitoring/js/lib/leaflet.fullscreen.min.js",
594
        ]
595
        css = {
11✔
596
            "all": [
597
                "monitoring/css/device-map.css",
598
                "leaflet/leaflet.css",
599
                "monitoring/css/leaflet.fullscreen.css",
600
                "monitoring/css/netjsongraph.css",
601
            ]
602
        }
603

604
    def has_module_permission(self, request):
11✔
605
        """Hide the model section from the admin index page."""
606
        return False
11✔
607

608
    def changelist_view(self, request, extra_context=None):
11✔
609
        loc_geojson_url = reverse_lazy(
×
610
            "monitoring:api_location_geojson", urlconf=MONITORING_API_URLCONF
611
        )
612
        device_list_url = reverse_lazy(
×
613
            "monitoring:api_location_device_list",
614
            urlconf=MONITORING_API_URLCONF,
615
            args=["000"],
616
        )
617
        indoor_coordinates_list_url = reverse_lazy(
×
618
            "monitoring:api_indoor_coordinates_list",
619
            urlconf=MONITORING_API_URLCONF,
620
            args=["000"],
621
        )
622
        extra_context = extra_context or {}
×
623
        extra_context.update(
×
624
            {
625
                "monitoring_device_list_url": device_list_url,
626
                "monitoring_location_geojson_url": loc_geojson_url,
627
                "monitoring_indoor_coordinates_list": indoor_coordinates_list_url,
628
                "monitoring_labels": json.dumps(app_settings.HEALTH_STATUS_LABELS),
629
                "monitoring_map_admin_url": reverse(
630
                    "admin:device_monitoring_map_changelist"
631
                ),
632
                # By default shows 'Select Map to change' heading making it empty to hide it
633
                "title": "",
634
            }
635
        )
636
        return super().changelist_view(request, extra_context=extra_context)
×
637

638

639
admin.site.register(MapPage, MapPageAdmin)
11✔
640

641

642
# Adding additonal js to add view on map buttons on DeviceLocationInline
643
def patch_device_location_inline(self):
11✔
644
    base = super(DeviceLocationInline, self).media
11✔
645
    extra = forms.Media(
11✔
646
        js=(
647
            "admin/js/jquery.init.js",
648
            "monitoring/js/location-inline.js",
649
        )
650
    )
651
    return base + extra
11✔
652

653

654
DeviceLocationInline.media = property(patch_device_location_inline)
11✔
655

656
admin.site.unregister(Device)
11✔
657
admin.site.register(Device, DeviceAdminExportable)
11✔
658

659
if app_settings.WIFI_SESSIONS_ENABLED:
11✔
660
    admin.site.register(WifiSession, WifiSessionAdmin)
11✔
661
    DeviceAdmin.conditional_inlines.append(WiFiSessionInline)
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc