Skip to content

Commit c756736

Browse files
DragnEmperorpandafynemesifier
committed
[change] Relevant templates: facilitate changing organization, optimize #204 #1050
Closes #204 Closes #1050 Signed-off-by: DragnEmperor <[email protected]> Co-authored-by: Gagan Deep <[email protected]> Co-authored-by: Federico Capoano <[email protected]>
1 parent d449e9b commit c756736

File tree

8 files changed

+525
-240
lines changed

8 files changed

+525
-240
lines changed

openwisp_controller/config/admin.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,13 @@ def get_fields(self, request, obj):
463463
fields = super().get_fields(request, obj)
464464
return self._error_reason_field_conditional(obj, fields)
465465

466+
def formfield_for_manytomany(self, db_field, request, **kwargs):
467+
# setting queryset none for all requests except POST as queryset
468+
# is required for the form to be valid
469+
if db_field.name == "templates" and request.method != "POST":
470+
kwargs["queryset"] = Template.objects.none()
471+
return super().formfield_for_manytomany(db_field, request, **kwargs)
472+
466473

467474
class ChangeDeviceGroupForm(forms.Form):
468475
device_group = forms.ModelChoiceField(
@@ -1319,6 +1326,13 @@ def get_extra_context(self, pk=None):
13191326
}
13201327
return ctx
13211328

1329+
def formfield_for_manytomany(self, db_field, request, **kwargs):
1330+
# setting queryset none for all requests except POST as queryset
1331+
# is required for the form to be valid
1332+
if db_field.name == "templates" and request.method != "POST":
1333+
kwargs["queryset"] = Template.objects.none()
1334+
return super().formfield_for_manytomany(db_field, request, **kwargs)
1335+
13221336

13231337
admin.site.register(Device, DeviceAdminExportable)
13241338
admin.site.register(Template, TemplateAdmin)

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

Lines changed: 80 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
"use strict";
22
django.jQuery(function ($) {
3-
var firstRun = true,
3+
var pageLoading = true,
44
backendFieldSelector = "#id_config-0-backend",
55
orgFieldSelector = "#id_organization",
66
isDeviceGroup = function () {
7-
return window._deviceGroup;
7+
return window._deviceGroupId !== undefined;
88
},
99
templatesFieldName = function () {
1010
return isDeviceGroup() ? "templates" : "config-0-templates";
1111
},
12+
isAddingNewObject = function () {
13+
return isDeviceGroup()
14+
? !$(".add-form").length
15+
: $('input[name="config-0-id"]').val().length === 0;
16+
},
1217
getTemplateOptionElement = function (
1318
index,
1419
templateId,
@@ -33,23 +38,15 @@ django.jQuery(function ($) {
3338
if (templateConfig.required) {
3439
inputField.prop("disabled", true);
3540
}
36-
if (isSelected || templateConfig.required) {
41+
// mark the template as selected if it is required or if it is enabled for the current device or group
42+
if (isSelected || templateConfig.required || templateConfig.selected) {
3743
inputField.prop("checked", true);
3844
}
3945
return element;
4046
},
4147
resetTemplateOptions = function () {
4248
$("ul.sortedm2m-items").empty();
4349
},
44-
updateTemplateSelection = function (selectedTemplates) {
45-
// Marks currently applied templates from database as selected
46-
// Only executed at page load.
47-
selectedTemplates.forEach(function (templateId) {
48-
$(
49-
`li.sortedm2m-item input[type="checkbox"][value="${templateId}"]:first`,
50-
).prop("checked", true);
51-
});
52-
},
5350
updateTemplateHelpText = function () {
5451
var helpText = "Choose items and order by drag & drop.";
5552
if ($("li.sortedm2m-item:first").length === 0) {
@@ -73,11 +70,32 @@ django.jQuery(function ($) {
7370
showRelevantTemplates();
7471
},
7572
updateConfigTemplateField = function (templates) {
76-
$(`input[name="${templatesFieldName()}"]`).attr(
77-
"value",
78-
templates.join(","),
79-
);
80-
$("input.sortedm2m:first").trigger("change");
73+
var value = templates.join(","),
74+
templateField = templatesFieldName(),
75+
updateInitialValue = false;
76+
$(`input[name="${templateField}"]`).attr("value", value);
77+
if (
78+
pageLoading ||
79+
// Handle cases where the AJAX request finishes after initial page load.
80+
// If we're editing an existing object and the initial value hasn't been set,
81+
// assign it now to avoid false positives in the unsaved changes warning.
82+
(!isAddingNewObject() &&
83+
django._owcInitialValues[templateField] === undefined)
84+
) {
85+
django._owcInitialValues[templateField] = value;
86+
updateInitialValue = true;
87+
}
88+
$("input.sortedm2m:first").trigger("change", {
89+
updateInitialValue: updateInitialValue,
90+
});
91+
},
92+
getSelectedTemplates = function () {
93+
// Returns the selected templates from the sortedm2m input
94+
var selectedTemplates = {};
95+
$("input.sortedm2m:checked").each(function (index, element) {
96+
selectedTemplates[$(element).val()] = $(element).prop("checked");
97+
});
98+
return selectedTemplates;
8199
},
82100
parseSelectedTemplates = function (selectedTemplates) {
83101
if (selectedTemplates !== undefined) {
@@ -88,85 +106,57 @@ django.jQuery(function ($) {
88106
}
89107
}
90108
},
109+
getRelevantTemplateUrl = function (orgID, backend) {
110+
// Returns the URL to fetch relevant templates
111+
var baseUrl = window._relevantTemplateUrl.replace("org_id", orgID);
112+
var url = new URL(baseUrl, window.location.origin);
113+
114+
// Get relevant templates of selected org and backend
115+
if (backend) {
116+
url.searchParams.set("backend", backend);
117+
}
118+
if (isDeviceGroup() && !$(".add-form").length) {
119+
url.searchParams.set("group_id", window._deviceGroupId);
120+
} else if ($('input[name="config-0-id"]').length) {
121+
url.searchParams.set("config_id", $('input[name="config-0-id"]').val());
122+
}
123+
return url.toString();
124+
},
91125
showRelevantTemplates = function () {
92126
var orgID = $(orgFieldSelector).val(),
93127
backend = isDeviceGroup() ? "" : $(backendFieldSelector).val(),
94-
selectedTemplates;
128+
currentSelection = getSelectedTemplates();
95129

96130
// Hide templates if no organization or backend is selected
97-
if (orgID.length === 0 || (!isDeviceGroup() && backend.length === 0)) {
131+
if (!orgID || (!isDeviceGroup() && backend.length === 0)) {
98132
resetTemplateOptions();
99133
updateTemplateHelpText();
100134
return;
101135
}
102136

103-
if (firstRun) {
104-
// selectedTemplates will be undefined on device add page or
105-
// when the user has changed any of organization or backend field.
106-
// selectedTemplates will be an empty string if no template is selected
107-
// ''.split(',') returns [''] hence, this case requires special handling
108-
selectedTemplates = isDeviceGroup()
109-
? parseSelectedTemplates($("#id_templates").val())
110-
: parseSelectedTemplates(
111-
django._owcInitialValues[templatesFieldName()],
112-
);
113-
}
114-
115-
var url = window._relevantTemplateUrl.replace("org_id", orgID);
116-
// Get relevant templates of selected org and backend
117-
url = url + "?backend=" + backend;
137+
var url = getRelevantTemplateUrl(orgID, backend);
118138
$.get(url).done(function (data) {
119139
resetTemplateOptions();
120140
var enabledTemplates = [],
121141
sortedm2mUl = $("ul.sortedm2m-items:first"),
122142
sortedm2mPrefixUl = $("ul.sortedm2m-items:last");
123143

124-
// Adds "li" elements for templates that are already selected
125-
// in the database. Select these templates and remove their key from "data"
126-
// This maintains the order of the templates and keep
127-
// enabled templates on the top
128-
if (selectedTemplates !== undefined) {
129-
selectedTemplates.forEach(function (templateId, index) {
130-
// corner case in which backend of template does not match
131-
if (!data[templateId]) {
132-
return;
133-
}
134-
var element = getTemplateOptionElement(
135-
index,
136-
templateId,
137-
data[templateId],
138-
true,
139-
false,
140-
),
141-
prefixElement = getTemplateOptionElement(
142-
index,
143-
templateId,
144-
data[templateId],
145-
true,
146-
true,
147-
);
148-
sortedm2mUl.append(element);
149-
if (!isDeviceGroup()) {
150-
sortedm2mPrefixUl.append(prefixElement);
151-
}
152-
delete data[templateId];
153-
});
154-
}
155-
156-
// Adds "li" elements for templates that are not selected
157-
// in the database.
158-
var counter =
159-
selectedTemplates !== undefined ? selectedTemplates.length : 0;
144+
// Adds "li" elements for templates
160145
Object.keys(data).forEach(function (templateId, index) {
161-
// corner case in which backend of template does not match
162-
if (!data[templateId]) {
163-
return;
164-
}
165-
index = index + counter;
166146
var isSelected =
167-
data[templateId].default &&
168-
selectedTemplates === undefined &&
169-
!data[templateId].required,
147+
// Template is selected in the database
148+
data[templateId].selected ||
149+
// Shared template which was already selected
150+
(currentSelection[templateId] !== undefined &&
151+
currentSelection[templateId]) ||
152+
// Default template should be selected when:
153+
// 1. A new object is created.
154+
// 2. Organization or backend field has changed.
155+
// (when the fields are changed, the currentSelection will be non-empty)
156+
(data[templateId].default &&
157+
(pageLoading ||
158+
isAddingNewObject() ||
159+
Object.keys(currentSelection).length > 0)),
170160
element = getTemplateOptionElement(
171161
index,
172162
templateId,
@@ -180,9 +170,6 @@ django.jQuery(function ($) {
180170
isSelected,
181171
true,
182172
);
183-
// Default templates should only be enabled for new
184-
// device or when user has changed any of organization
185-
// or backend field
186173
if (isSelected === true) {
187174
enabledTemplates.push(templateId);
188175
}
@@ -191,14 +178,20 @@ django.jQuery(function ($) {
191178
sortedm2mPrefixUl.append(prefixElement);
192179
}
193180
});
194-
if (firstRun === true && selectedTemplates !== undefined) {
195-
updateTemplateSelection(selectedTemplates);
196-
}
197181
updateTemplateHelpText();
198182
updateConfigTemplateField(enabledTemplates);
199183
});
200184
},
185+
initTemplateField = function () {
186+
// sortedm2m generates a hidden input dynamically using rendered input checkbox elements,
187+
// but because the queryset is set to None in the Django admin, the input is created
188+
// without a name attribute. This workaround assigns the correct name to the hidden input.
189+
$('.sortedm2m-container input[type="hidden"][id="undefined"]')
190+
.first()
191+
.attr("name", templatesFieldName());
192+
},
201193
bindDefaultTemplateLoading = function () {
194+
initTemplateField();
202195
var backendField = $(backendFieldSelector);
203196
$(orgFieldSelector).change(function () {
204197
// Only fetch templates when backend field is present
@@ -211,16 +204,18 @@ django.jQuery(function ($) {
211204
addChangeEventHandlerToBackendField();
212205
} else if (isDeviceGroup()) {
213206
// Initially request data to get templates
207+
initTemplateField();
214208
showRelevantTemplates();
215209
} else {
216210
// Add view: backendField is added when user adds configuration
217211
$("#config-group > fieldset.module").ready(function () {
218212
$("div.add-row > a").one("click", function () {
213+
initTemplateField();
219214
addChangeEventHandlerToBackendField();
220215
});
221216
});
222217
}
223-
firstRun = false;
218+
pageLoading = false;
224219
$("#content-main form").submit(function () {
225220
$(
226221
'ul.sortedm2m-items:first input[type="checkbox"][data-required="true"]',

openwisp_controller/config/static/config/js/widget.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,8 +494,8 @@
494494
getDefaultValues(true);
495495
});
496496
}
497-
$(".sortedm2m-items").on("change", function () {
498-
getDefaultValues();
497+
$(".sortedm2m-items").on("change", function (event, data) {
498+
getDefaultValues(data && data.updateInitialValue === true);
499499
});
500500
$(".sortedm2m-items").on("sortstop", function () {
501501
getDefaultValues();

openwisp_controller/config/templates/admin/device_group/change_form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
(function ($) {
1313
$(document).ready( function () {
1414
window._relevantTemplateUrl = "{{ relevant_template_url | safe }}";
15-
window._deviceGroup = true;
15+
window._deviceGroupId = "{{ original.pk }}";
1616
window.bindDefaultTemplateLoading();
1717
})
1818
}) (django.jQuery);

0 commit comments

Comments
 (0)