diff --git a/openwisp_controller/config/admin.py b/openwisp_controller/config/admin.py index 7c328a353..d9e376564 100644 --- a/openwisp_controller/config/admin.py +++ b/openwisp_controller/config/admin.py @@ -66,6 +66,15 @@ from django.contrib.admin import ModelAdmin +def is_recover_view(request): + resolver_match = getattr(request, "resolver_match", None) + return getattr(request, "_recover_view", False) or bool( + resolver_match + and resolver_match.url_name + and resolver_match.url_name.endswith("_recover") + ) + + class SystemDefinedVariableMixin(object): def system_context(self, obj): system_context = obj.get_system_context() @@ -349,6 +358,12 @@ class Meta: class ConfigForm(AlwaysHasChangedMixin, BaseForm): _old_templates = None + readonly_backend = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.readonly_backend and "backend" in self.fields: + self.fields["backend"].disabled = True def get_temp_model_instance(self, **options): config_model = self.Meta.model @@ -457,6 +472,17 @@ class ConfigInline( verbose_name_plural = verbose_name multitenant_shared_relations = ("templates",) + def get_formset(self, request, obj=None, **kwargs): + readonly_backend = bool( + obj and obj._has_config() and not is_recover_view(request) + ) + kwargs["form"] = type( + "ConfigInlineForm", + (self.form,), + {"readonly_backend": readonly_backend}, + ) + return super().get_formset(request, obj, **kwargs) + def get_queryset(self, request): qs = super().get_queryset(request) return qs.select_related(*self.change_select_related) @@ -468,7 +494,14 @@ def _error_reason_field_conditional(self, obj, fields): return fields def get_readonly_fields(self, request, obj): - fields = super().get_readonly_fields(request, obj) + fields = list(super().get_readonly_fields(request, obj)) + if ( + obj + and obj._has_config() + and not is_recover_view(request) + and "backend" not in fields + ): + fields.append("backend") return self._error_reason_field_conditional(obj, fields) def get_fields(self, request, obj): @@ -1082,6 +1115,12 @@ class TemplateAdmin(MultitenantAdminMixin, BaseConfigAdmin, SystemDefinedVariabl readonly_fields = ["system_context"] autocomplete_fields = ["vpn"] + def get_readonly_fields(self, request, obj=None): + fields = list(super().get_readonly_fields(request, obj)) + if obj and not is_recover_view(request) and "backend" not in fields: + fields.append("backend") + return fields + @admin.action(permissions=["add"]) def clone_selected_templates(self, request, queryset): selectable_orgs = None @@ -1261,6 +1300,12 @@ class VpnAdmin( "modified", ] + def get_readonly_fields(self, request, obj=None): + fields = list(super().get_readonly_fields(request, obj)) + if obj and not is_recover_view(request) and "backend" not in fields: + fields.append("backend") + return fields + class Media(BaseConfigAdmin): js = list(BaseConfigAdmin.Media.js) + [f"{prefix}js/vpn.js"] diff --git a/openwisp_controller/config/static/config/js/relevant_templates.js b/openwisp_controller/config/static/config/js/relevant_templates.js index 9d7a1a766..69d3da45b 100644 --- a/openwisp_controller/config/static/config/js/relevant_templates.js +++ b/openwisp_controller/config/static/config/js/relevant_templates.js @@ -10,9 +10,11 @@ django.jQuery(function ($) { return isDeviceGroup() ? "templates" : "config-0-templates"; }, isAddingNewObject = function () { - return isDeviceGroup() - ? !$(".add-form").length - : $('input[name="config-0-id"]').val().length === 0; + if (isDeviceGroup()) { + return $(".add-form").length > 0; + } + var configIdField = $('input[name="config-0-id"]'); + return !configIdField.length || configIdField.val().length === 0; }, getTemplateOptionElement = function ( index, @@ -123,11 +125,12 @@ django.jQuery(function ($) { }, showRelevantTemplates = function () { var orgID = $(orgFieldSelector).val(), - backend = isDeviceGroup() ? "" : $(backendFieldSelector).val(), + backend = isDeviceGroup() ? "" : $(backendFieldSelector).val() || "", + configID = $('input[name="config-0-id"]').val(), currentSelection = getSelectedTemplates(); // Hide templates if no organization or backend is selected - if (!orgID || (!isDeviceGroup() && backend.length === 0)) { + if (!orgID || (!isDeviceGroup() && backend.length === 0 && !configID)) { resetTemplateOptions(); updateTemplateHelpText(); return; @@ -193,14 +196,23 @@ django.jQuery(function ($) { initTemplateField(); var backendField = $(backendFieldSelector); $(orgFieldSelector).change(function () { - // Only fetch templates when backend field is present - if ($(backendFieldSelector).length > 0 || isDeviceGroup()) { + // Fetch templates when backend can be determined either from + // an editable backend field or from an existing config object. + if ( + $(backendFieldSelector).length > 0 || + isDeviceGroup() || + !isAddingNewObject() + ) { showRelevantTemplates(); } }); // Change view: backendField is rendered on page load if (backendField.length > 0) { addChangeEventHandlerToBackendField(); + } else if (!isDeviceGroup() && !isAddingNewObject()) { + // Change view for device config has readonly backend with no input element. + // In this case the backend is inferred server-side from config_id. + showRelevantTemplates(); } else if (isDeviceGroup()) { // Initially request data to get templates initTemplateField(); diff --git a/openwisp_controller/config/static/config/js/vpn.js b/openwisp_controller/config/static/config/js/vpn.js index 68f368184..ae6ff44c0 100644 --- a/openwisp_controller/config/static/config/js/vpn.js +++ b/openwisp_controller/config/static/config/js/vpn.js @@ -33,12 +33,29 @@ django.jQuery(function ($) { return el.parents(".form-row").eq(0); }; + var getBackendValue = function () { + var backendInput = $("#id_backend"); + if (backendInput.length && backendInput.val() !== undefined) { + return String(backendInput.val()).toLocaleLowerCase(); + } + var readonlyBackendEl = $(".field-backend .readonly").first(); + if (!readonlyBackendEl.length) { + return ""; + } + var readonlyBackend = readonlyBackendEl.data("backend"); + if ( + readonlyBackend === undefined || + readonlyBackend === null || + String(readonlyBackend).trim() === "" + ) { + readonlyBackend = readonlyBackendEl.text().trim(); + } + return String(readonlyBackend).toLocaleLowerCase(); + }; + var toggleRelatedFields = function () { // Show IP and Subnet field only for WireGuard backend - var backendValue = - $("#id_backend").val() === undefined - ? "" - : $("#id_backend").val().toLocaleLowerCase().toLocaleLowerCase(), + var backendValue = getBackendValue(), op; if (backendValue.includes("wireguard") || backendValue.includes("vxlan")) { op = "show"; @@ -62,10 +79,12 @@ django.jQuery(function ($) { }; // clean config when VPN backend is changed - $("#id_backend").change(function () { - $("#id_config").val("{}"); - toggleRelatedFields(); - }); + if ($("#id_backend").length) { + $("#id_backend").change(function () { + $("#id_config").val("{}"); + toggleRelatedFields(); + }); + } toggleRelatedFields(); }); diff --git a/openwisp_controller/config/static/config/js/widget.js b/openwisp_controller/config/static/config/js/widget.js index 6b034711b..e3706f0bd 100644 --- a/openwisp_controller/config/static/config/js/widget.js +++ b/openwisp_controller/config/static/config/js/widget.js @@ -413,6 +413,57 @@ }); }; + var getReadonlySchemaKey = function (schemas) { + var readonlyBackendEl = $(".field-backend .readonly").first(); + if (!readonlyBackendEl.length) { + return false; + } + var backendValue = String( + readonlyBackendEl.data("backend") || readonlyBackendEl.attr("data-backend") || "", + ).trim(); + if (backendValue) { + if (schemas[backendValue] !== undefined) { + return backendValue; + } + var normalizedBackendValue = backendValue.toLocaleLowerCase(), + directSchemaKey = false; + $.each(Object.keys(schemas), function (index, key) { + if (String(key).toLocaleLowerCase() === normalizedBackendValue) { + directSchemaKey = key; + return false; + } + }); + if (directSchemaKey) { + return directSchemaKey; + } + } + // Fallback for deployments that do not expose data-backend yet. + // Match readonly backend labels to schema keys using normalize(). + var backendLabel = readonlyBackendEl.text().trim(); + if (!backendLabel) { + return false; + } + var normalize = function (value) { + return String(value) + .toLocaleLowerCase() + .replace(/[^a-z0-9]/g, ""); + }; + var normalizedBackendLabel = normalize(backendLabel); + var schemaKey = false; + $.each(Object.keys(schemas), function (index, key) { + var normalizedBackendKey = normalize(key.split(".").pop()); + if ( + normalizedBackendLabel === normalizedBackendKey || + normalizedBackendLabel.includes(normalizedBackendKey) || + normalizedBackendKey.includes(normalizedBackendLabel) + ) { + schemaKey = key; + return false; + } + }); + return schemaKey; + }; + var bindLoadUi = function () { $('.jsoneditor-raw:not([name*="__prefix__"]):not(.manual)').each(function (i, el) { // Add query parameters defined in the widget @@ -439,7 +490,12 @@ schemaSelector = "#id_backend, #id_config-0-backend"; } var selector = $(schemaSelector), + schemaKey = false; + if (selector.length) { schemaKey = selector.val() || false; + } else { + schemaKey = getReadonlySchemaKey(schemas); + } // load first time loadUi(el, schemaKey, schemas, true); // reload when selector is changed diff --git a/openwisp_controller/config/templates/admin/config/change_form.html b/openwisp_controller/config/templates/admin/config/change_form.html index c4099580e..82c9da42b 100644 --- a/openwisp_controller/config/templates/admin/config/change_form.html +++ b/openwisp_controller/config/templates/admin/config/change_form.html @@ -64,6 +64,16 @@ {% block content %}