Skip to content

Commit b7aa2d6

Browse files
committed
[feature] Add a new map page
1 parent 480383c commit b7aa2d6

File tree

6 files changed

+147
-27
lines changed

6 files changed

+147
-27
lines changed

openwisp_monitoring/device/admin.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.contrib.contenttypes.models import ContentType
99
from django.forms import ModelForm
1010
from django.templatetags.static import static
11-
from django.urls import resolve, reverse
11+
from django.urls import resolve, reverse, reverse_lazy, path
1212
from django.utils import timezone
1313
from django.utils.formats import localize
1414
from django.utils.html import format_html
@@ -45,6 +45,7 @@
4545
Notification = load_model("openwisp_notifications", "Notification")
4646
Check = load_model("check", "Check")
4747
Organization = load_model("openwisp_users", "Organization")
48+
MapPage = load_model("device_monitoring", "Map")
4849

4950

5051
class CheckInlineFormSet(BaseGenericInlineFormSet):
@@ -569,6 +570,69 @@ def has_delete_permission(self, request, obj=None):
569570
return super(admin.ModelAdmin, self).has_delete_permission(request, obj)
570571

571572

573+
class MapPageAdmin(MultitenantAdminMixin, admin.ModelAdmin):
574+
"""
575+
Overrides the changelist template of proxy Model Map to render
576+
a full-screen interactive map using custom template map_page.html.
577+
"""
578+
579+
change_list_template = "admin/map/map_page.html"
580+
581+
class Media:
582+
js = [
583+
'monitoring/js/lib/netjsongraph.min.js',
584+
'monitoring/js/lib/leaflet.fullscreen.min.js',
585+
]
586+
css = {
587+
'all': [
588+
'monitoring/css/device-map.css',
589+
'leaflet/leaflet.css',
590+
'monitoring/css/leaflet.fullscreen.css',
591+
'monitoring/css/netjsongraph.css',
592+
]
593+
}
594+
595+
def has_module_permission(self, request):
596+
"""Hide the model section from the admin index page."""
597+
return False
598+
599+
def changelist_view(self, request, extra_context=None):
600+
loc_geojson_url = reverse_lazy(
601+
'monitoring:api_location_geojson', urlconf=MONITORING_API_URLCONF
602+
)
603+
device_list_url = reverse_lazy(
604+
'monitoring:api_location_device_list',
605+
urlconf=MONITORING_API_URLCONF,
606+
args=['000'],
607+
)
608+
indoor_coordinates_list_url = reverse_lazy(
609+
"monitoring:api_indoor_coordinates_list",
610+
urlconf=MONITORING_API_URLCONF,
611+
args=["000"],
612+
)
613+
if MONITORING_API_BASEURL:
614+
loc_geojson_url = urljoin(MONITORING_API_BASEURL, str(loc_geojson_url))
615+
device_list_url = urljoin(MONITORING_API_BASEURL, str(device_list_url))
616+
indoor_coordinates_list_url = urljoin(
617+
MONITORING_API_BASEURL, str(indoor_coordinates_list_url)
618+
)
619+
extra_context = extra_context or {}
620+
extra_context.update(
621+
{
622+
"monitoring_device_list_url": device_list_url,
623+
"monitoring_location_geojson_url": loc_geojson_url,
624+
"monitoring_indoor_coordinates_list": indoor_coordinates_list_url,
625+
"monitoring_labels": app_settings.HEALTH_STATUS_LABELS,
626+
# By default shows 'Select Map to change' heading making it empty to hide it
627+
'title': '',
628+
}
629+
)
630+
return super().changelist_view(request, extra_context=extra_context)
631+
632+
633+
admin.site.register(MapPage, MapPageAdmin)
634+
635+
# Adding additonal js to add view on map buttons on DeviceLocationInline
572636
def patch_device_location_inline(self):
573637
base = super(DeviceLocationInline, self).media
574638
extra = forms.Media(

openwisp_monitoring/device/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
)
1010

1111
BaseDevice = load_model("config", "Device", require_ready=False)
12-
12+
BaseLocation = load_model("geo", "Location", require_ready=False)
1313

1414
class DeviceData(AbstractDeviceData, BaseDevice):
1515
checks = GenericRelation(get_model_name("check", "Check"))
@@ -36,3 +36,10 @@ class WifiSession(AbstractWifiSession):
3636
class Meta(AbstractWifiSession.Meta):
3737
abstract = False
3838
swappable = swappable_setting("device_monitoring", "WifiSession")
39+
40+
41+
class Map(BaseLocation):
42+
class Meta:
43+
proxy = True
44+
abstract = False
45+
swappable = swappable_setting("device_monitoring", "Map")

openwisp_monitoring/device/static/monitoring/js/lib/netjsongraph.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openwisp_monitoring/device/static/monitoring/js/location-inline.js

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,50 @@
22

33
(function ($) {
44
$(document).ready(function () {
5-
const location_parent = $("fieldset.module.aligned.loci.coords");
6-
const floorplan_parent = $("fieldset.module.aligned.indoor.coords");
5+
const locationParent = $("fieldset.module.aligned.loci.coords");
6+
const floorplanParent = $("fieldset.module.aligned.indoor.coords");
77
const deviceLocationId = $("#id_devicelocation-0-id").val();
88
const locationId = $("#id_devicelocation-0-location").val();
99
const floor = $("#id_devicelocation-0-floor").val();
1010
const geoMapId = "dashboard-geo-map";
1111
const indoorMapId = `${locationId}:${floor}`;
1212

13-
const open_location_btn = `
13+
const openLocationBtn = `
1414
<div class="form-row field-location-view-button" style="display: block;">
1515
<div>
16-
<div class="flex-container">
17-
<label for="id_devicelocation-0-address">View on Map:</label>
18-
<a href="/#id=${geoMapId}&nodeId=${locationId}"
16+
<div class="flex-container">
17+
<a href="/admin/device_monitoring/map#id=${geoMapId}&nodeId=${locationId}"
1918
id="open-location-btn"
2019
class="default-btn"
2120
style="color: white; text-decoration: none;">
22-
Open Location
21+
Open Location on Map
2322
</a>
2423
</div>
2524
</div>
2625
</div>
2726
`;
2827

29-
const open_indoor_device_btn = `
28+
const openIndoorDeviceBtn = `
3029
<div class="form-row field-indoor-view-button" style="display: block;">
3130
<div>
32-
<div class="flex-container">
33-
<label for="id_devicelocation-0-address">View on Indoor Map:</label>
34-
<a href="/#id=${indoorMapId}&nodeId=${deviceLocationId}"
31+
<div class="flex-container">
32+
<a href="/admin/device_monitoring/map#id=${indoorMapId}&nodeId=${deviceLocationId}"
3533
id="open-indoor-device-btn"
3634
class="default-btn"
3735
style="color: white; text-decoration: none;">
38-
Open Device
36+
Open Device on Map
3937
</a>
4038
</div>
4139
</div>
4240
</div>
4341
`;
4442

4543
if (locationId) {
46-
location_parent.append(open_location_btn);
44+
locationParent.append(openLocationBtn);
4745
}
4846

4947
if (floor && locationId) {
50-
floorplan_parent.append(open_indoor_device_btn);
48+
floorplanParent.append(openIndoorDeviceBtn);
5149
}
5250
});
5351
})(django.jQuery);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{% extends "admin/base_site.html" %}
2+
{% load static leaflet_tags i18n %}
3+
4+
{% block title %} {% trans 'Map' %} {% endblock %}
5+
6+
{% block breadcrumbs %}
7+
<div class="breadcrumbs">
8+
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a><span>{% trans 'Map' %}</span>
9+
</div>
10+
{% endblock %}
11+
12+
{% block extrahead %}
13+
{{ block.super }}
14+
{{ media }}
15+
<script>
16+
window._owGeoMapConfig = {
17+
geoJsonUrl: '{{ monitoring_location_geojson_url }}',
18+
locationDeviceUrl: '{{ monitoring_device_list_url }}',
19+
indoorCoordinatesUrl: '{{ monitoring_indoor_coordinates_list }}',
20+
labels: {{ monitoring_labels | safe }}
21+
}
22+
</script>
23+
<style>
24+
#device-map-container {
25+
width: 100%;
26+
height: 87.7vh;
27+
margin: 15px auto 0 auto;
28+
}
29+
</style>
30+
{% endblock %}
31+
32+
{% block content %}
33+
<div id='leaflet-config'>
34+
{% autoescape on %}{% leaflet_json_config %}{% endautoescape %}
35+
</div>
36+
<div id="dashboard-map-overlay">
37+
<div id="device-map-container">
38+
<div class="no-data">
39+
<p>{% trans 'No map data to show' %}.</p>
40+
<p><a class="button submit" href="#close">Close</a></p>
41+
</div>
42+
<div class="ow-loading-spinner"></div>
43+
</div>
44+
</div>
45+
<script src="{% static 'monitoring/js/device-map.js' %}"></script>
46+
<script src="{% static 'monitoring/js/floorplan.js' %}"></script>
47+
{% endblock %}

openwisp_monitoring/tests/test_selenium.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -767,11 +767,13 @@ def test_redirect_to_map_view_from_device_location_inline(self):
767767
"#open-location-btn",
768768
timeout=5,
769769
).click()
770-
current_hash = self.web_driver.execute_script(
771-
"return window.location.hash;"
772-
)
773770
expected_hash = f"#id={mapId}&nodeId={location.id}"
774-
self.assertEqual(expected_hash, current_hash)
771+
try:
772+
WebDriverWait(self.web_driver, 5).until(
773+
EC.url_to_be(f"{self.live_server_url}{expected_hash}")
774+
)
775+
except TimeoutException:
776+
self.fail("Failed redirecting to map page")
775777
popup = self.find_element(By.CSS_SELECTOR, ".map-detail", timeout=5)
776778
logs = self.get_browser_logs()
777779
self.assertEqual(len(logs), 0)
@@ -799,11 +801,13 @@ def test_redirect_to_map_view_from_device_location_inline(self):
799801
"#open-indoor-device-btn",
800802
timeout=5,
801803
).click()
802-
current_hash = self.web_driver.execute_script(
803-
"return window.location.hash;"
804-
)
805804
expected_hash = f"#id={indoorMapId}&nodeId={device_location.id}"
806-
self.assertEqual(expected_hash, current_hash)
805+
try:
806+
WebDriverWait(self.web_driver, 5).until(
807+
EC.url_to_be(f"{self.live_server_url}{expected_hash}")
808+
)
809+
except TimeoutException:
810+
self.fail("Failed redirecting to map page")
807811
popup = self.find_element(By.CSS_SELECTOR, ".njg-tooltip-inner", timeout=5)
808812
logs = self.get_browser_logs()
809813
self.assertEqual(len(logs), 0)

0 commit comments

Comments
 (0)