Skip to content

Commit a9c5fbc

Browse files
authored
[feature] Estimated location: added notification and admin warning #1035
Closes #1035 Signed-off-by: DragnEmperor <[email protected]>
1 parent 889dde3 commit a9c5fbc

File tree

12 files changed

+220
-58
lines changed

12 files changed

+220
-58
lines changed

docs/user/estimated-location.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,15 @@ but only if the existing location is estimated.
5454
If two devices share the same IP address and are assigned to the same
5555
location, and the last IP of one of the devices is updated, the system
5656
will create a new estimated location for that device.
57+
58+
Visibility of Estimated Status
59+
------------------------------
60+
61+
The estimated status of a location is visible on the location page if the
62+
feature is enabled for the organization. The location admin page also
63+
includes indicators for the estimated status.
64+
65+
- The name of the location will have suffix **(Estimated Location :
66+
<ip_address>)**.
67+
- A warning on top of the page.
68+
- **Is Estimated** field.

openwisp_controller/config/whois/tasks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ class WHOISCeleryRetryTask(OpenwispCeleryTask):
4646
def on_failure(self, exc, task_id, args, kwargs, einfo):
4747
"""Notify the user about the failure of the WHOIS task."""
4848
device_pk = kwargs.get("device_pk")
49-
send_whois_task_notification(device_pk=device_pk, notify_type="device_error")
49+
send_whois_task_notification(
50+
device_pk=device_pk, notify_type="whois_device_error"
51+
)
5052
logger.error(f"WHOIS lookup failed. Details: {exc}")
5153
return super().on_failure(exc, task_id, args, kwargs, einfo)
5254

openwisp_controller/config/whois/test_whois.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ def assert_logging_on_exception(
614614
"Failed to fetch WHOIS details for device",
615615
notification.message,
616616
)
617-
self.assertIn(device.last_ip, notification.description)
617+
self.assertIn(device.last_ip, notification.rendered_description)
618618

619619
mock_info.reset_mock()
620620
mock_warn.reset_mock()

openwisp_controller/config/whois/utils.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,57 @@
1-
from django.utils.translation import gettext as _
1+
from django.utils.translation import gettext_lazy as _
22
from openwisp_notifications.signals import notify
33
from swapper import load_model
44

55
from .. import settings as app_settings
66

77
MESSAGE_MAP = {
8-
"device_error": {
8+
"whois_device_error": {
9+
"type": "generic_message",
10+
"level": "error",
911
"message": _(
1012
"Failed to fetch WHOIS details for device"
1113
" [{notification.target}]({notification.target_link})"
1214
),
1315
"description": _("WHOIS details could not be fetched for ip: {ip_address}."),
14-
"level": "error",
1516
},
16-
"location_error": {
17+
"estimated_location_error": {
18+
"level": "error",
19+
"type": "estimated_location_info",
1720
"message": _(
1821
"Unable to create estimated location for device "
1922
"[{notification.target}]({notification.target_link}). "
2023
"Please assign/create a location manually."
2124
),
2225
"description": _("Multiple devices found for IP: {ip_address}"),
23-
"level": "error",
26+
},
27+
"estimated_location_created": {
28+
"type": "estimated_location_info",
29+
"description": _("Estimated Location {notification.verb} for IP: {ip_address}"),
30+
},
31+
"estimated_location_updated": {
32+
"type": "estimated_location_info",
33+
"message": _(
34+
"Estimated location [{notification.actor}]({notification.actor_link})"
35+
" for device"
36+
" [{notification.target}]({notification.target_link})"
37+
" updated successfully."
38+
),
39+
"description": _("Estimated Location updated for IP: {ip_address}"),
2440
},
2541
}
2642

2743

28-
def send_whois_task_notification(device_pk, notify_type):
44+
def send_whois_task_notification(device_pk, notify_type, actor=None):
2945
Device = load_model("config", "Device")
3046

3147
device = Device.objects.get(pk=device_pk)
3248
notify_details = MESSAGE_MAP[notify_type]
3349
notify.send(
34-
sender=device,
35-
type="generic_message",
50+
sender=actor or device,
3651
target=device,
3752
action_object=device,
38-
level=notify_details["level"],
39-
message=notify_details["message"],
40-
description=notify_details["description"].format(ip_address=device.last_ip),
53+
ip_address=device.last_ip,
54+
**notify_details,
4155
)
4256

4357

openwisp_controller/geo/admin.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,30 @@ class LocationAdmin(MultitenantAdminMixin, AbstractLocationAdmin):
9999
form = LocationForm
100100
inlines = [FloorPlanInline]
101101
list_select_related = ("organization",)
102-
readonly_fields = ("is_estimated",)
102+
change_form_template = "admin/geo/location/change_form.html"
103103

104104
def get_fields(self, request, obj=None):
105105
fields = super().get_fields(request, obj)
106106
if obj and not check_estimate_location_configured(obj.organization_id):
107-
fields.remove("is_estimated")
107+
if "is_estimated" in fields:
108+
fields.remove("is_estimated")
108109
return fields
109110

111+
def get_readonly_fields(self, request, obj=None):
112+
fields = super().get_readonly_fields(request, obj)
113+
if obj and check_estimate_location_configured(obj.organization_id):
114+
fields = fields + ("is_estimated",)
115+
return fields
116+
117+
def change_view(self, request, object_id, form_url="", extra_context=None):
118+
obj = self.get_object(request, object_id)
119+
extra_context = {
120+
"estimated_configured": check_estimate_location_configured(
121+
obj.organization_id
122+
)
123+
}
124+
return super().change_view(request, object_id, form_url, extra_context)
125+
110126

111127
LocationAdmin.list_display.insert(1, "organization")
112128
LocationAdmin.list_filter.insert(0, MultitenantOrgFilter)

openwisp_controller/geo/apps.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from openwisp_utils.admin_theme import register_dashboard_chart
99
from openwisp_utils.admin_theme.menu import register_menu_group
1010

11+
from .estimated_location.handlers import register_estimated_location_notification_types
12+
1113

1214
class GeoConfig(LociConfig):
1315
name = "openwisp_controller.geo"
@@ -21,6 +23,7 @@ def ready(self):
2123
super().ready()
2224
self.register_dashboard_charts()
2325
self.register_menu_groups()
26+
register_estimated_location_notification_types()
2427
if getattr(settings, "TESTING", False):
2528
self._add_params_to_test_config()
2629

openwisp_controller/geo/base/models.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ class BaseLocation(OrgMixin, AbstractLocation):
2222
class Meta(AbstractLocation.Meta):
2323
abstract = True
2424

25+
def __init__(self, *args, **kwargs):
26+
super().__init__(*args, **kwargs)
27+
self._initial_is_estimated = self.is_estimated
28+
2529
def clean(self):
26-
if self.is_estimated and not check_estimate_location_configured(
27-
self.organization_id
30+
# Raise validation error if `is_estimated` is True but estimated feature is
31+
# disabled.
32+
if (
33+
(self._state.adding or self._initial_is_estimated != self.is_estimated)
34+
and self.is_estimated
35+
and not check_estimate_location_configured(self.organization_id)
2836
):
2937
raise ValidationError(
3038
{
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from django.utils.translation import gettext_lazy as _
2+
from openwisp_notifications.types import register_notification_type
3+
from swapper import load_model
4+
5+
from openwisp_controller.config import settings as config_app_settings
6+
7+
8+
def register_estimated_location_notification_types():
9+
"""
10+
Register the notification types used by the Estimated Location module.
11+
This is necessary to ensure that the notifications are properly configured.
12+
"""
13+
if not config_app_settings.WHOIS_CONFIGURED:
14+
return
15+
16+
Device = load_model("config", "Device")
17+
Location = load_model("geo", "Location")
18+
19+
register_notification_type(
20+
"estimated_location_info",
21+
{
22+
"verbose_name": _("Estimated Location INFO"),
23+
"verb": _("created"),
24+
"level": "info",
25+
"email_subject": _(
26+
"Estimated Location: Created for device {notification.target}"
27+
),
28+
"message": _(
29+
"Estimated location [{notification.actor}]({notification.actor_link})"
30+
" for device"
31+
" [{notification.target}]({notification.target_link})"
32+
" {notification.verb} successfully."
33+
),
34+
"target_link": (
35+
"openwisp_controller.geo.estimated_location.utils"
36+
".get_device_location_notification_target_url"
37+
),
38+
"email_notification": False,
39+
},
40+
models=[Device, Location],
41+
)

openwisp_controller/geo/estimated_location/tasks.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def _create_estimated_location(device_location, location_defaults):
4444
f"Estimated location saved successfully for {device_pk}"
4545
f" for IP: {ip_address}"
4646
)
47+
send_whois_task_notification(
48+
device_pk=device_pk,
49+
notify_type="estimated_location_created",
50+
actor=location,
51+
)
4752

4853
def _update_or_create_estimated_location(
4954
device_location, whois_obj, attached_devices_exists=False
@@ -72,6 +77,11 @@ def _update_or_create_estimated_location(
7277
f"Estimated location saved successfully for {device_pk}"
7378
f" for IP: {ip_address}"
7479
)
80+
send_whois_task_notification(
81+
device_pk=device_pk,
82+
notify_type="estimated_location_updated",
83+
actor=current_location,
84+
)
7585
elif not current_location:
7686
# If there is no current location, we create a new one.
7787
_create_estimated_location(device_location, location_defaults)
@@ -97,7 +107,7 @@ def _handle_attach_existing_location(
97107
# the user to resolve the conflict manually.
98108
if devices_with_location.count() > 1:
99109
send_whois_task_notification(
100-
device_pk=device_pk, notify_type="location_error"
110+
device_pk=device_pk, notify_type="estimated_location_error"
101111
)
102112
logger.error(
103113
"Multiple devices with locations found with same "
@@ -123,6 +133,11 @@ def _handle_attach_existing_location(
123133
f"Estimated location saved successfully for {device_pk}"
124134
f" for IP: {ip_address}"
125135
)
136+
send_whois_task_notification(
137+
device_pk=device_pk,
138+
notify_type="estimated_location_updated",
139+
actor=existing_location,
140+
)
126141

127142
whois_obj = WHOISInfo.objects.filter(ip_address=ip_address).first()
128143
device = (
@@ -146,3 +161,8 @@ def _handle_attach_existing_location(
146161
_handle_attach_existing_location(
147162
device, device_location, whois_obj, attached_devices_exists
148163
)
164+
else:
165+
logger.info(
166+
f"Non Estimated location already set for {device_pk}. Update"
167+
f" location manually as per IP: {ip_address}"
168+
)

0 commit comments

Comments
 (0)