Skip to content

Commit c8856a4

Browse files
committed
[change] Improved relevant templates
1 parent 41c1b40 commit c8856a4

File tree

4 files changed

+320
-180
lines changed

4 files changed

+320
-180
lines changed

openwisp_controller/config/static/config/js/relevant_templates.js

Lines changed: 43 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,35 @@ django.jQuery(function ($) {
8989
}
9090
}
9191
},
92+
getRelevantTemplateUrl = function (orgID, backend) {
93+
// Returns the URL to fetch relevant templates
94+
var baseUrl = window._relevantTemplateUrl.replace("org_id", orgID);
95+
var url = new URL(baseUrl, window.location.origin);
96+
97+
// Get relevant templates of selected org and backend
98+
if (backend) {
99+
url.searchParams.set("backend", backend);
100+
}
101+
if (isDeviceGroup() && !$(".add-form").length) {
102+
// Get the group id from the URL
103+
// TODO: This is fragile, consider using a more robust way to get the group id.
104+
var pathParts = window.location.pathname.split("/");
105+
url.searchParams.set("group_id", pathParts[pathParts.length - 3]);
106+
} else if ($('input[name="config-0-device"]').length) {
107+
url.searchParams.set(
108+
"device_id",
109+
$('input[name="config-0-device"]').val(),
110+
);
111+
}
112+
return url.toString();
113+
},
92114
showRelevantTemplates = function () {
93115
var orgID = $(orgFieldSelector).val(),
94116
backend = isDeviceGroup() ? "" : $(backendFieldSelector).val(),
95117
selectedTemplates;
96118

97119
// Hide templates if no organization or backend is selected
98-
if (
99-
(orgID && orgID.length === 0) ||
100-
(!isDeviceGroup() && backend.length === 0)
101-
) {
120+
if (!orgID || (!isDeviceGroup() && backend.length === 0)) {
102121
resetTemplateOptions();
103122
updateTemplateHelpText();
104123
return;
@@ -109,77 +128,24 @@ django.jQuery(function ($) {
109128
// when the user has changed any of organization or backend field.
110129
// selectedTemplates will be an empty string if no template is selected
111130
// ''.split(',') returns [''] hence, this case requires special handling
112-
selectedTemplates = isDeviceGroup()
113-
? parseSelectedTemplates($("#id_templates").val())
114-
: parseSelectedTemplates(
115-
django._owcInitialValues[templatesFieldName()],
116-
);
117-
}
118-
119-
var url = window._relevantTemplateUrl.replace("org_id", orgID);
120-
// Get relevant templates of selected org and backend
121-
url = url + "?backend=" + backend;
122-
if (!isDeviceGroup()) {
123-
var deviceID = $('input[name="config-0-device"]').val();
124-
url = url + "&device=" + deviceID;
125-
} else {
126-
// Get the group id from the URL
127-
var pathParts = window.location.pathname.split("/");
128-
url = url + "&group=" + pathParts[pathParts.length - 3];
131+
selectedTemplates = parseSelectedTemplates(
132+
$('input[name="' + templatesFieldName() + '"]').val(),
133+
);
129134
}
135+
var url = getRelevantTemplateUrl(orgID, backend);
130136
$.get(url).done(function (data) {
131137
resetTemplateOptions();
132138
var enabledTemplates = [],
133139
sortedm2mUl = $("ul.sortedm2m-items:first"),
134140
sortedm2mPrefixUl = $("ul.sortedm2m-items:last");
135141

136-
// Adds "li" elements for templates that are already selected
137-
// in the database. Select these templates and remove their key from "data"
138-
// This maintains the order of the templates and keep
139-
// enabled templates on the top
140-
if (selectedTemplates !== undefined) {
141-
selectedTemplates.forEach(function (templateId, index) {
142-
// corner case in which backend of template does not match
143-
if (!data[templateId]) {
144-
return;
145-
}
146-
var element = getTemplateOptionElement(
147-
index,
148-
templateId,
149-
data[templateId],
150-
true,
151-
false,
152-
),
153-
prefixElement = getTemplateOptionElement(
154-
index,
155-
templateId,
156-
data[templateId],
157-
true,
158-
true,
159-
);
160-
sortedm2mUl.append(element);
161-
if (!isDeviceGroup()) {
162-
sortedm2mPrefixUl.append(prefixElement);
163-
}
164-
delete data[templateId];
165-
});
166-
}
167-
168-
// Adds "li" elements for templates that are not selected
169-
// in the database.
170-
var counter =
171-
selectedTemplates !== undefined ? selectedTemplates.length : 0,
172-
deviceTemplates = [];
142+
// Adds "li" elements for templates
173143
Object.keys(data).forEach(function (templateId, index) {
174-
// corner case in which backend of template does not match
175-
if (!data[templateId]) {
176-
return;
177-
}
178-
index = index + counter;
179144
var isSelected =
180-
data[templateId].default &&
181-
selectedTemplates === undefined &&
182-
!data[templateId].required,
145+
data[templateId].selected ||
146+
(data[templateId].default &&
147+
selectedTemplates === undefined &&
148+
!data[templateId].required),
183149
element = getTemplateOptionElement(
184150
index,
185151
templateId,
@@ -199,9 +165,6 @@ django.jQuery(function ($) {
199165
if (isSelected === true) {
200166
enabledTemplates.push(templateId);
201167
}
202-
if (element.children().children("input").prop("checked") === true) {
203-
deviceTemplates.push(templateId);
204-
}
205168
sortedm2mUl.append(element);
206169
if (!isDeviceGroup()) {
207170
sortedm2mPrefixUl.append(prefixElement);
@@ -210,30 +173,20 @@ django.jQuery(function ($) {
210173
if (firstRun === true && selectedTemplates !== undefined) {
211174
updateTemplateSelection(selectedTemplates);
212175
}
213-
// this runs on first load and sets the name of hidden input field which tracks
214-
// the selected templates.
215-
if (selectedTemplates === undefined) {
216-
if (!isDeviceGroup()) {
217-
$(
218-
`#config-0 .field-templates .sortedm2m-container input[type="hidden"]`,
219-
)
220-
.first()
221-
.attr("name", templatesFieldName());
222-
// set the initial value of the hidden input field
223-
// to the selected templates
224-
django._owcInitialValues[templatesFieldName()] =
225-
deviceTemplates.join(",");
226-
} else {
227-
$(
228-
`.field-templates .sortedm2m-container input[type="hidden"]`,
229-
).attr("name", templatesFieldName());
230-
}
231-
}
232176
updateTemplateHelpText();
233177
updateConfigTemplateField(enabledTemplates);
234178
});
235179
},
180+
initTemplateField = function () {
181+
// sortedm2m generates a hidden input dynamically using rendered input checkbox elements,
182+
// but because the queryset is set to None in the Django admin, the input is created
183+
// without a name attribute. This workaround assigns the correct name to the hidden input.
184+
$('.sortedm2m-container input[type="hidden"][id="undefined"]')
185+
.first()
186+
.attr("name", templatesFieldName());
187+
},
236188
bindDefaultTemplateLoading = function () {
189+
initTemplateField();
237190
var backendField = $(backendFieldSelector);
238191
$(orgFieldSelector).change(function () {
239192
// Only fetch templates when backend field is present
@@ -246,11 +199,13 @@ django.jQuery(function ($) {
246199
addChangeEventHandlerToBackendField();
247200
} else if (isDeviceGroup()) {
248201
// Initially request data to get templates
202+
initTemplateField();
249203
showRelevantTemplates();
250204
} else {
251205
// Add view: backendField is added when user adds configuration
252206
$("#config-group > fieldset.module").ready(function () {
253207
$("div.add-row > a").one("click", function () {
208+
initTemplateField();
254209
addChangeEventHandlerToBackendField();
255210
});
256211
});

openwisp_controller/config/tests/test_admin.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,85 @@ def test_add_vpn(self):
15681568
response, 'value="openwisp_controller.vpn_backends.OpenVpn" selected'
15691569
)
15701570

1571+
def test_vpn_clients_deleted(self):
1572+
def _update_template(templates):
1573+
params.update(
1574+
{
1575+
'config-0-templates': ','.join(
1576+
[str(template.pk) for template in templates]
1577+
)
1578+
}
1579+
)
1580+
response = self.client.post(path, data=params, follow=True)
1581+
self.assertEqual(response.status_code, 200)
1582+
return response
1583+
1584+
vpn = self._create_vpn()
1585+
template = self._create_template()
1586+
vpn_template = self._create_template(
1587+
name='vpn-test',
1588+
type='vpn',
1589+
vpn=vpn,
1590+
auto_cert=True,
1591+
)
1592+
cert_query = Cert.objects.exclude(pk=vpn.cert_id)
1593+
valid_cert_query = cert_query.filter(revoked=False)
1594+
revoked_cert_query = cert_query.filter(revoked=True)
1595+
1596+
# Add a new device
1597+
path = reverse(f'admin:{self.app_label}_device_add')
1598+
params = self._get_device_params(org=self._get_org())
1599+
response = self.client.post(path, data=params, follow=True)
1600+
self.assertEqual(response.status_code, 200)
1601+
1602+
config = Device.objects.get(name=params['name']).config
1603+
self.assertEqual(config.vpnclient_set.count(), 0)
1604+
self.assertEqual(config.templates.count(), 0)
1605+
1606+
path = reverse(f'admin:{self.app_label}_device_change', args=[config.device_id])
1607+
params.update(
1608+
{
1609+
'config-0-id': str(config.pk),
1610+
'config-0-device': str(config.device_id),
1611+
'config-INITIAL_FORMS': 1,
1612+
'_continue': True,
1613+
}
1614+
)
1615+
1616+
with self.subTest('Adding only VpnClient template'):
1617+
# Adding VpnClient template to the device
1618+
_update_template(templates=[vpn_template])
1619+
1620+
self.assertEqual(config.templates.count(), 1)
1621+
self.assertEqual(config.vpnclient_set.count(), 1)
1622+
self.assertEqual(cert_query.count(), 1)
1623+
self.assertEqual(valid_cert_query.count(), 1)
1624+
1625+
# Remove VpnClient template from the device
1626+
_update_template(templates=[])
1627+
1628+
self.assertEqual(config.templates.count(), 0)
1629+
self.assertEqual(config.vpnclient_set.count(), 0)
1630+
# Removing VPN template marks the related certificate as revoked
1631+
self.assertEqual(revoked_cert_query.count(), 1)
1632+
self.assertEqual(valid_cert_query.count(), 0)
1633+
1634+
with self.subTest('Add VpnClient template along with another template'):
1635+
# Adding templates to the device
1636+
_update_template(templates=[template, vpn_template])
1637+
1638+
self.assertEqual(config.templates.count(), 2)
1639+
self.assertEqual(config.vpnclient_set.count(), 1)
1640+
self.assertEqual(valid_cert_query.count(), 1)
1641+
1642+
# Remove VpnClient template from the device
1643+
_update_template(templates=[template])
1644+
1645+
self.assertEqual(config.templates.count(), 1)
1646+
self.assertEqual(config.vpnclient_set.count(), 0)
1647+
self.assertEqual(valid_cert_query.count(), 0)
1648+
self.assertEqual(revoked_cert_query.count(), 2)
1649+
15711650
def test_ip_not_in_add_device(self):
15721651
path = reverse(f'admin:{self.app_label}_device_add')
15731652
response = self.client.get(path)

0 commit comments

Comments
 (0)