Allow admins to disable the expert mode form #296
8 changed files with 72 additions and 8 deletions
|
|
@ -322,7 +322,7 @@ class ServiceDefinitionAdmin(admin.ModelAdmin):
|
||||||
(
|
(
|
||||||
_("Form Configuration"),
|
_("Form Configuration"),
|
||||||
{
|
{
|
||||||
"fields": ("form_config",),
|
"fields": ("form_config", "hide_expert_mode"),
|
||||||
"description": _(
|
"description": _(
|
||||||
"Optional custom form configuration. When provided, this will be used instead of auto-generating the form from the OpenAPI spec."
|
"Optional custom form configuration. When provided, this will be used instead of auto-generating the form from the OpenAPI spec."
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ class Migration(migrations.Migration):
|
||||||
name="user_info",
|
name="user_info",
|
||||||
field=models.JSONField(
|
field=models.JSONField(
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='Array of info objects: [{"title": "…", "content": "…", "help_text": "…"}]. The help_text field is optional and will be shown as a hover popover on an info icon.',
|
help_text=(
|
||||||
|
'Array of info objects: [{"title": "…", "content": "…", "help_text": "…"}]. '
|
||||||
|
"The help_text field is optional and will be shown as a hover popover on an info icon."
|
||||||
|
),
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="User Information",
|
verbose_name="User Information",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 5.2.8 on 2025-11-14 15:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0014_hide_billing_address"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="servicedefinition",
|
||||||
|
name="hide_expert_mode",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=(
|
||||||
|
"When enabled, the 'Show Expert Mode' toggle will be hidden and only the custom form configuration will be available. "
|
||||||
|
"Only applies when a custom form configuration is provided."
|
||||||
|
),
|
||||||
|
verbose_name="Disable Expert Mode",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -370,6 +370,14 @@ class ServiceDefinition(ServalaModelMixin, models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
hide_expert_mode = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("Disable Expert Mode"),
|
||||||
|
help_text=_(
|
||||||
|
"When enabled, the 'Show Expert Mode' toggle will be hidden and only the custom form "
|
||||||
|
"configuration will be available. Only applies when a custom form configuration is provided."
|
||||||
|
),
|
||||||
|
)
|
||||||
service = models.ForeignKey(
|
service = models.ForeignKey(
|
||||||
to="Service",
|
to="Service",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<a href="{{ instance.urls.base }}" class="btn btn-secondary me-1 mb-1">{% translate "Back" %}</a>
|
<a href="{{ instance.urls.base }}" class="btn btn-secondary me-1 mb-1">{% translate "Back" %}</a>
|
||||||
{% endblock page_title_extra %}
|
{% endblock page_title_extra %}
|
||||||
{% partialdef service-form %}
|
{% partialdef service-form %}
|
||||||
{% if form %}
|
{% if form or custom_form %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex align-items-center"></div>
|
<div class="card-header d-flex align-items-center"></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
{% if not form %}
|
{% if not form and not custom_form %}
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
{% translate "Cannot update this service instance because its details could not be retrieved from the underlying system. It might have been deleted externally." %}
|
{% translate "Cannot update this service instance because its details could not be retrieved from the underlying system. It might have been deleted externally." %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
{% if form_action %}action="{{ form_action }}"{% endif %}>
|
{% if form_action %}action="{{ form_action }}"{% endif %}>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "frontend/forms/errors.html" %}
|
{% include "frontend/forms/errors.html" %}
|
||||||
{% if form %}
|
{% if form and expert_form and not hide_expert_mode %}
|
||||||
<div class="mb-3 text-end">
|
<div class="mb-3 text-end">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="text-muted small"
|
class="text-muted small"
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if expert_form %}
|
{% if expert_form and not hide_expert_mode %}
|
||||||
<div id="expert-form-container"
|
<div id="expert-form-container"
|
||||||
class="expert-crd-form"
|
class="expert-crd-form"
|
||||||
style="{% if form %}display:none{% endif %}">
|
style="{% if form %}display:none{% endif %}">
|
||||||
|
|
@ -144,6 +144,6 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<script defer src="{% static 'js/bootstrap-tabs.js' %}"></script>
|
<script defer src="{% static 'js/bootstrap-tabs.js' %}"></script>
|
||||||
{% if form %}
|
{% if form and not hide_expert_mode %}
|
||||||
<script defer src="{% static 'js/expert-mode.js' %}"></script>
|
<script defer src="{% static 'js/expert-mode.js' %}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,11 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_instance_form(self, ignore_data=False):
|
def get_instance_form(self, ignore_data=False):
|
||||||
if not self.context_object or not self.context_object.model_form_class:
|
if (
|
||||||
|
not self.context_object
|
||||||
|
or not self.context_object.model_form_class
|
||||||
|
or self.hide_expert_mode
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
return self.context_object.model_form_class(
|
return self.context_object.model_form_class(
|
||||||
|
|
@ -187,11 +191,21 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
# vs "expert form" = auto-generated (all technical fields)
|
# vs "expert form" = auto-generated (all technical fields)
|
||||||
return self.request.POST.get("active_form", "expert") == "custom"
|
return self.request.POST.get("active_form", "expert") == "custom"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def hide_expert_mode(self):
|
||||||
|
return (
|
||||||
|
self.context_object
|
||||||
|
and self.context_object.service_definition
|
||||||
|
and self.context_object.service_definition.form_config
|
||||||
|
and self.context_object.service_definition.hide_expert_mode
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["select_form"] = self.select_form
|
context["select_form"] = self.select_form
|
||||||
context["has_control_planes"] = self.planes.exists()
|
context["has_control_planes"] = self.planes.exists()
|
||||||
context["selected_plane"] = self.selected_plane
|
context["selected_plane"] = self.selected_plane
|
||||||
|
context["hide_expert_mode"] = self.hide_expert_mode
|
||||||
if self.request.method == "POST":
|
if self.request.method == "POST":
|
||||||
if self.is_custom_form:
|
if self.is_custom_form:
|
||||||
context["service_form"] = self.get_instance_form(ignore_data=True)
|
context["service_form"] = self.get_instance_form(ignore_data=True)
|
||||||
|
|
@ -441,6 +455,8 @@ class ServiceInstanceUpdateView(
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_form(self, *args, ignore_data=False, **kwargs):
|
def get_form(self, *args, ignore_data=False, **kwargs):
|
||||||
|
if self.hide_expert_mode:
|
||||||
|
return
|
||||||
if not ignore_data:
|
if not ignore_data:
|
||||||
return super().get_form(*args, **kwargs)
|
return super().get_form(*args, **kwargs)
|
||||||
cls = self.get_form_class()
|
cls = self.get_form_class()
|
||||||
|
|
@ -477,8 +493,19 @@ class ServiceInstanceUpdateView(
|
||||||
return self.form_valid(form)
|
return self.form_valid(form)
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def hide_expert_mode(self):
|
||||||
|
return (
|
||||||
|
self.object
|
||||||
|
and self.object.context
|
||||||
|
and self.object.context.service_definition
|
||||||
|
and self.object.context.service_definition.form_config
|
||||||
|
and self.object.context.service_definition.hide_expert_mode
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["hide_expert_mode"] = self.hide_expert_mode
|
||||||
if self.request.method == "POST":
|
if self.request.method == "POST":
|
||||||
if self.is_custom_form:
|
if self.is_custom_form:
|
||||||
context["custom_form"] = self.get_custom_form()
|
context["custom_form"] = self.get_custom_form()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import pytest
|
||||||
|
|
||||||
from servala.core.models.service import CloudProvider, ServiceOffering
|
from servala.core.models.service import CloudProvider, ServiceOffering
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"url,redirect",
|
"url,redirect",
|
||||||
(
|
(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue