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

openwisp / openwisp-monitoring / 22103372064

17 Feb 2026 02:56PM UTC coverage: 98.329% (-0.2%) from 98.561%
22103372064

Pull #738

github

web-flow
Merge fda77db16 into 331fca2bf
Pull Request #738: [feature] GSoC25: General Map: Indoor, Mobile, Linkable URLs

3884 of 3950 relevant lines covered (98.33%)

10.82 hits per line

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

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

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

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

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

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

51

52
class CheckInlineFormSet(BaseGenericInlineFormSet):
12✔
53
    def full_clean(self):
12✔
54
        for form in self.forms:
12✔
55
            obj = form.instance
12✔
56
            if not obj.content_type or not obj.object_id:
12✔
57
                setattr(
12✔
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)
12✔
63
        super().full_clean()
12✔
64

65

66
class InlinePermissionMixin:
12✔
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):
12✔
79
        return super(admin.options.InlineModelAdmin, self).has_add_permission(
12✔
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):
12✔
86
        return super(admin.options.InlineModelAdmin, self).has_change_permission(
12✔
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):
12✔
93
        return super(admin.options.InlineModelAdmin, self).has_view_permission(
12✔
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):
12✔
100
        return super(admin.options.InlineModelAdmin, self).has_delete_permission(
12✔
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(
12✔
108
    DeactivatedDeviceReadOnlyMixin,
109
    InlinePermissionMixin,
110
):
111
    def has_add_permission(self, request, obj=None):
12✔
112
        perm = super().has_add_permission(request, obj)
12✔
113
        return self._has_permission(request, obj, perm)
12✔
114

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

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

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

127

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

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

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

152

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

164
    def _post_clean(self):
12✔
165
        self.instance._delete_instance = False
12✔
166
        if all(
12✔
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
12✔
178
        super()._post_clean()
12✔
179

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

186

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

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

198

199
class MetricInline(
12✔
200
    DeactivatedDeviceReadOnlyInlinePermissionMixin, NestedGenericStackedInline
201
):
202
    model = Metric
12✔
203
    extra = 0
12✔
204
    inlines = [AlertSettingsInline]
12✔
205
    fieldsets = [
12✔
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"]
12✔
222
    # Explicitly changed name from Metrics to Alert Settings
223
    verbose_name = _("Alert Settings")
12✔
224
    verbose_name_plural = verbose_name
12✔
225
    inline_permission_suffix = "alertsettings_inline"
12✔
226
    # Ordering queryset by metric name
227
    ordering = ("name",)
12✔
228

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

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

242
    def has_add_permission(self, request, obj=None):
12✔
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
12✔
246

247
    def has_delete_permission(self, request, obj=None):
12✔
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
12✔
251

252

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

261
    class Media:
12✔
262
        js = (
12✔
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 = {
12✔
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):
12✔
282
        ctx = super().get_extra_context(pk)
12✔
283
        if pk:
12✔
284
            device_data = DeviceData(pk=uuid.UUID(pk))
12✔
285
            api_url = reverse(
12✔
286
                "monitoring:api_device_metric",
287
                urlconf=MONITORING_API_URLCONF,
288
                args=[pk],
289
            )
290
            if MONITORING_API_BASEURL:
12✔
291
                api_url = urljoin(MONITORING_API_BASEURL, api_url)
12✔
292
            ctx.update(
12✔
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
12✔
301

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

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

316
    def health_status(self, obj):
12✔
317
        return format_html(
12✔
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")
12✔
324

325
    def get_object(self, request, object_id, from_field=None):
12✔
326
        obj = super().get_object(request, object_id, from_field=from_field)
12✔
327
        if obj and obj.wifisession_set.exists():
12✔
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 = {
12✔
332
                "wifisession_set-TOTAL_FORMS": "1",
333
                "wifisession_set-INITIAL_FORMS": "1",
334
            }
335
            request.POST = request.POST.copy()
12✔
336
            request.POST.update(wifisession_formset_data)
12✔
337
        return obj
12✔
338

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

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

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

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

384

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

391

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

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

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

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

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

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

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

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

420
    wmm.short_description = "WMM"
12✔
421

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

425
    wds.short_description = "WDS"
12✔
426

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

430
    wps.short_description = "WPS"
12✔
431

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

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

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

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

446
    def related_organization(self, obj):
12✔
447
        app_label = Organization._meta.app_label
12✔
448
        url = reverse(
12✔
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>')
12✔
452

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

455

456
class WiFiSessionInline(WifiSessionAdminHelperMixin, admin.TabularInline):
12✔
457
    model = WifiSession
12✔
458
    fk_name = "device"
12✔
459
    fields = [
12✔
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
12✔
471
    can_delete = False
12✔
472
    extra = 0
12✔
473
    template = "admin/monitoring/device/wifisession_tabular.html"
12✔
474

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

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

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

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

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

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

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

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

507

508
class WifiSessionAdmin(
12✔
509
    WifiSessionAdminHelperMixin, MultitenantAdminMixin, ReadOnlyAdmin
510
):
511
    multitenant_parent = "device"
12✔
512
    model = WifiSession
12✔
513
    list_display = [
12✔
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 = [
12✔
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"]
12✔
543
    list_filter = [
12✔
544
        DeviceOrganizationFilter,
545
        DeviceFilter,
546
        DeviceGroupFilter,
547
        "start_time",
548
        "stop_time",
549
    ]
550

551
    def get_readonly_fields(self, request, obj=None):
12✔
552
        fields = super().get_readonly_fields(request, obj)
12✔
553
        fields += [
12✔
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
12✔
568

569
    def get_queryset(self, request):
12✔
570
        return (
12✔
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):
12✔
579
        return super(admin.ModelAdmin, self).has_delete_permission(request, obj)
12✔
580

581

582
class MapPageAdmin(MultitenantAdminMixin, admin.ModelAdmin):
12✔
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"
12✔
589

590
    class Media:
12✔
591
        js = [
12✔
592
            "monitoring/js/lib/netjsongraph.min.js",
593
            "monitoring/js/lib/leaflet.fullscreen.min.js",
594
        ]
595
        css = {
12✔
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):
12✔
605
        """Hide the model section from the admin index page."""
606
        return False
12✔
607

608
    def changelist_view(self, request, extra_context=None):
12✔
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
        if MONITORING_API_BASEURL:
×
623
            loc_geojson_url = urljoin(MONITORING_API_BASEURL, str(loc_geojson_url))
×
624
            device_list_url = urljoin(MONITORING_API_BASEURL, str(device_list_url))
×
625
            indoor_coordinates_list_url = urljoin(
×
626
                MONITORING_API_BASEURL, str(indoor_coordinates_list_url)
627
            )
628
        extra_context = extra_context or {}
×
629
        extra_context.update(
×
630
            {
631
                "monitoring_device_list_url": device_list_url,
632
                "monitoring_location_geojson_url": loc_geojson_url,
633
                "monitoring_indoor_coordinates_list": indoor_coordinates_list_url,
634
                "monitoring_labels": json.dumps(app_settings.HEALTH_STATUS_LABELS),
635
                # By default shows 'Select Map to change' heading making it empty to hide it
636
                "title": "",
637
            }
638
        )
639
        return super().changelist_view(request, extra_context=extra_context)
×
640

641

642
admin.site.register(MapPage, MapPageAdmin)
12✔
643

644

645
# Adding additional js to add view on map buttons on DeviceLocationInline
646
def patch_device_location_inline(self):
12✔
647
    base = super(DeviceLocationInline, self).media
12✔
648
    extra = forms.Media(
12✔
649
        js=(
650
            "admin/js/jquery.init.js",
651
            "monitoring/js/location-inline.js",
652
        ),
653
        css={"all": ("monitoring/css/monitoring.css",)},
654
    )
655
    return base + extra
12✔
656

657

658
DeviceLocationInline.media = property(patch_device_location_inline)
12✔
659

660
admin.site.unregister(Device)
12✔
661
admin.site.register(Device, DeviceAdminExportable)
12✔
662

663
if app_settings.WIFI_SESSIONS_ENABLED:
12✔
664
    admin.site.register(WifiSession, WifiSessionAdmin)
12✔
665
    DeviceAdmin.conditional_inlines.append(WiFiSessionInline)
12✔
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