Skip to content

Commit 13e94a6

Browse files
committed
[fix] Allowed deleting device with "deactivating" config status #949
Fixes #949
1 parent aa08611 commit 13e94a6

File tree

5 files changed

+93
-4
lines changed

5 files changed

+93
-4
lines changed

openwisp_controller/config/admin.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -566,8 +566,6 @@ def has_delete_permission(self, request, obj=None):
566566
perm = super().has_delete_permission(request)
567567
if not obj:
568568
return perm
569-
if obj._has_config():
570-
perm = perm and obj.config.is_deactivated()
571569
return perm and obj.is_deactivated()
572570

573571
def save_form(self, request, form, change):
@@ -900,6 +898,17 @@ def recover_view(self, request, version_id, extra_context=None):
900898
request._recover_view = True
901899
return super().recover_view(request, version_id, extra_context)
902900

901+
def delete_view(self, request, object_id, extra_context=None):
902+
extra_context = extra_context or {}
903+
obj = self.get_object(request, object_id)
904+
if obj and obj._has_config() and not obj.config.is_deactivated():
905+
extra_context['deactivating_warning'] = True
906+
return super().delete_view(request, object_id, extra_context)
907+
908+
def delete_model(self, request, obj):
909+
force_delete = request.POST.get('force_delete') == 'true'
910+
obj.delete(check_deactivated=not force_delete)
911+
903912
def get_inlines(self, request, obj):
904913
inlines = super().get_inlines(request, obj)
905914
# this only makes sense in existing devices

openwisp_controller/config/api/views.py

+4
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ class DeviceDetailView(ProtectedAPIMixin, RetrieveUpdateDestroyAPIView):
107107
queryset = Device.objects.select_related('config', 'group', 'organization')
108108
permission_classes = ProtectedAPIMixin.permission_classes + (DevicePermission,)
109109

110+
def perform_destroy(self, instance):
111+
force_deletion = self.request.query_params.get('force', None) == 'true'
112+
instance.delete(check_deactivated=(not force_deletion))
113+
110114

111115
class DeviceActivateView(ProtectedAPIMixin, GenericAPIView):
112116
serializer_class = serializers.Serializer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{% extends "admin/delete_confirmation.html" %}
2+
{% load i18n %}
3+
4+
{% block extrastyle %}
5+
{{ block.super }}
6+
<style>
7+
#deactivating-warning .warning p {
8+
margin-top: 0px;
9+
}
10+
</style>
11+
{% endblock extrastyle %}
12+
13+
{% block delete_confirm %}
14+
{% if deactivating_warning %}
15+
<div id="deactivating-warning">
16+
<ul class="messagelist">
17+
<li class="warning">
18+
<p>{% trans "The device is still in the deactivating state, meaning its configuration is still present on the device. If you wish to remove the configuration from the device, please wait until the config status changes to deactivated. Proceeding will delete the device from OpenWISP without ensuring its configuration has been removed." %}</p>
19+
<form>
20+
<input type="submit" class="button danger-btn" id="warning-ack"
21+
value="{% trans "I understand the risks, delete the device" %}">
22+
<a class="button cancel-link">No, take me back</a>
23+
</form>
24+
</li>
25+
</ul>
26+
</div>
27+
{% endif %}
28+
<div id="delete-confirm-container" {% if deactivating_warning %}style="display:none;"{% endif %}>
29+
<p>{% blocktranslate with escaped_object=object %}Are you sure you want to delete the {{ object_name }}
30+
"{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktranslate %}</p>
31+
{% include "admin/includes/object_delete_summary.html" %}
32+
<h2>{% translate "Objects" %}</h2>
33+
<ul id="deleted-objects">{{ deleted_objects|unordered_list }}</ul>
34+
<form method="post">{% csrf_token %}
35+
<div>
36+
<input type="hidden" name="post" value="yes">
37+
<input type="hidden" name="force_delete" value="false">
38+
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
39+
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
40+
<input type="submit" value="{% translate 'Yes, I’m sure' %}">
41+
<a href="#" class="button cancel-link">{% translate "No, take me back" %}</a>
42+
</div>
43+
</form>
44+
</div>
45+
{% endblock %}
46+
47+
{% block footer %}
48+
{{ block.super }}
49+
<script>
50+
(function ($) {
51+
$(document).ready(function () {
52+
$('#warning-ack').click(function (event) {
53+
event.preventDefault();
54+
$('#deactivating-warning').slideUp('fast');
55+
$('#delete-confirm-container').slideDown('fast');
56+
$('input[name="force_delete"]').val('true');
57+
});
58+
})
59+
})(django.jQuery);
60+
</script>
61+
{% endblock %}

openwisp_controller/config/tests/test_admin.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2120,9 +2120,8 @@ def test_device_with_config_change_deactivate_deactivate(self):
21202120
)
21212121
# Save buttons are absent on deactivated device
21222122
self.assertNotContains(response, self._save_btn_html)
2123-
# Delete button is not present if config status is deactivating
21242123
self.assertEqual(device.config.status, 'deactivating')
2125-
self.assertNotContains(response, delete_btn_html)
2124+
self.assertContains(response, delete_btn_html)
21262125
self.assertNotContains(response, self._deactivate_btn_html)
21272126
self.assertContains(response, self._activate_btn_html)
21282127
# Verify adding a new DeviceLocation and DeviceConnection is not allowed

openwisp_controller/config/tests/test_api.py

+16
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,22 @@ def test_device_delete_api(self):
539539
self.assertEqual(response.status_code, 204)
540540
self.assertEqual(Device.objects.count(), 0)
541541

542+
def test_deactivating_device_force_deletion(self):
543+
self._create_template(required=True)
544+
device = self._create_device()
545+
config = self._create_config(device=device)
546+
device.deactivate()
547+
path = reverse('config_api:device_detail', args=[device.pk])
548+
549+
with self.subTest(
550+
'Test force deleting device with config in deactivating state'
551+
):
552+
self.assertEqual(device.is_deactivated(), True)
553+
self.assertEqual(config.is_deactivating(), True)
554+
response = self.client.delete(f'{path}?force=true')
555+
self.assertEqual(response.status_code, 204)
556+
self.assertEqual(Device.objects.count(), 0)
557+
542558
def test_template_create_no_org_api(self):
543559
self.assertEqual(Template.objects.count(), 0)
544560
path = reverse('config_api:template_list')

0 commit comments

Comments
 (0)