Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion openwisp_monitoring/device/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import uuid
from urllib.parse import urljoin

from django import forms
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericStackedInline
from django.contrib.contenttypes.forms import BaseGenericInlineFormSet
from django.contrib.contenttypes.models import ContentType
from django.forms import ModelForm
from django.templatetags.static import static
from django.urls import resolve, reverse
from django.urls import resolve, reverse, reverse_lazy
from django.utils import timezone
from django.utils.formats import localize
from django.utils.html import format_html
Expand All @@ -24,6 +25,7 @@

from openwisp_controller.config.admin import DeactivatedDeviceReadOnlyMixin
from openwisp_controller.config.admin import DeviceAdmin as BaseDeviceAdmin
from openwisp_controller.geo.admin import DeviceLocationInline
from openwisp_users.multitenancy import MultitenantAdminMixin
from openwisp_utils.admin import ReadOnlyAdmin

Expand All @@ -43,6 +45,7 @@
Notification = load_model("openwisp_notifications", "Notification")
Check = load_model("check", "Check")
Organization = load_model("openwisp_users", "Organization")
MapPage = load_model("device_monitoring", "Map")


class CheckInlineFormSet(BaseGenericInlineFormSet):
Expand Down Expand Up @@ -575,6 +578,77 @@ def has_delete_permission(self, request, obj=None):
return super(admin.ModelAdmin, self).has_delete_permission(request, obj)


class MapPageAdmin(MultitenantAdminMixin, admin.ModelAdmin):
"""
Overrides the changelist template of proxy Model Map to render
a full-screen interactive map using custom template map_page.html.
"""

change_list_template = "admin/map/map_page.html"

class Media:
js = [
"monitoring/js/lib/netjsongraph.min.js",
"monitoring/js/lib/leaflet.fullscreen.min.js",
]
css = {
"all": [
"monitoring/css/device-map.css",
"leaflet/leaflet.css",
"monitoring/css/leaflet.fullscreen.css",
"monitoring/css/netjsongraph.css",
]
}

def has_module_permission(self, request):
"""Hide the model section from the admin index page."""
return False

def changelist_view(self, request, extra_context=None):
loc_geojson_url = reverse_lazy(
"monitoring:api_location_geojson", urlconf=MONITORING_API_URLCONF
)
device_list_url = reverse_lazy(
"monitoring:api_location_device_list",
urlconf=MONITORING_API_URLCONF,
args=["000"],
)
indoor_coordinates_list_url = reverse_lazy(
"monitoring:api_indoor_coordinates_list",
urlconf=MONITORING_API_URLCONF,
args=["000"],
)
extra_context = extra_context or {}
extra_context.update(
{
"monitoring_device_list_url": device_list_url,
"monitoring_location_geojson_url": loc_geojson_url,
"monitoring_indoor_coordinates_list": indoor_coordinates_list_url,
"monitoring_labels": app_settings.HEALTH_STATUS_LABELS,
# By default shows 'Select Map to change' heading making it empty to hide it
"title": "",
}
)
return super().changelist_view(request, extra_context=extra_context)


admin.site.register(MapPage, MapPageAdmin)


# Adding additonal js to add view on map buttons on DeviceLocationInline
def patch_device_location_inline(self):
base = super(DeviceLocationInline, self).media
extra = forms.Media(
js=(
"admin/js/jquery.init.js",
"monitoring/js/location-inline.js",
)
)
return base + extra


DeviceLocationInline.media = property(patch_device_location_inline)

admin.site.unregister(Device)
admin.site.register(Device, DeviceAdminExportable)

Expand Down
26 changes: 26 additions & 0 deletions openwisp_monitoring/device/migrations/0010_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.2.6 on 2025-10-22 10:44

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("device_monitoring", "0009_update_device_status_for_disabled_critical_checks"),
("geo", "0003_alter_devicelocation_floorplan_location"),
]

operations = [
migrations.CreateModel(
name="Map",
fields=[],
options={
"abstract": False,
"proxy": True,
"swappable": "DEVICE_MONITORING_MAP_MODEL",
"indexes": [],
"constraints": [],
},
bases=("geo.location",),
),
]
8 changes: 8 additions & 0 deletions openwisp_monitoring/device/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)

BaseDevice = load_model("config", "Device", require_ready=False)
BaseLocation = load_model("geo", "Location", require_ready=False)


class DeviceData(AbstractDeviceData, BaseDevice):
Expand Down Expand Up @@ -36,3 +37,10 @@ class WifiSession(AbstractWifiSession):
class Meta(AbstractWifiSession.Meta):
abstract = False
swappable = swappable_setting("device_monitoring", "WifiSession")


class Map(BaseLocation):
class Meta:
proxy = True
abstract = False
swappable = swappable_setting("device_monitoring", "Map")
42 changes: 39 additions & 3 deletions openwisp_monitoring/device/static/monitoring/js/device-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,11 @@
});
$(".floorplan-btn").on("click", function () {
const floorplanUrl = getIndoorCoordinatesUrl(locationId);
window.openFloorPlan(floorplanUrl);
window.openFloorPlan(floorplanUrl, locationId);
});
el.find(".leaflet-popup-close-button").on("click", function () {
const id = netjsongraphInstance.config.bookmarkableActions.id;
netjsongraphInstance.utils.removeUrlFragment(id);
});
loadingOverlay.hide();
},
Expand Down Expand Up @@ -329,6 +333,14 @@
],
},
},
bookmarkableActions: {
enabled: true,
id: "dashboard-geo-map",
zoom: {
enabled: true,
zoomLevel: 10,
},
},
mapTileConfig: tiles,
nodeCategories: Object.keys(STATUS_COLORS).map((status) => ({
name: status,
Expand Down Expand Up @@ -535,8 +547,32 @@
},
// Added to open popup for a specific location Id in selenium tests
openPopup: function (locationId) {
const nodeData = map?.data?.nodes?.find((n) => n.id === locationId);
loadPopUpContent(nodeData, map);
const index = map?.data?.nodes?.findIndex((n) => n.id === locationId);
const nodeData = map?.data?.nodes?.[index];
if (index === -1 || !nodeData) {
const id = map.config.bookmarkableActions.id;
map.utils.removeUrlFragment(id);
console.error(`Node with ID "${locationId}" not found.`);
return;
}
const option = map.echarts.getOption();
const series = option.series.find(
(s) => s.type === "scatter" || "effectScatter",
);
const seriesIndex = option.series.indexOf(series);

const params = {
componentType: "series",
componentSubType: series.type,
dataIndex: index,
data: {
...series.data[index],
node: nodeData,
},
seriesIndex: seriesIndex,
seriesType: series.type,
};
map.echarts.trigger("click", params);
},
});
map.render();
Expand Down
Loading
Loading