Generate custom service form from config
This commit is contained in:
parent
357e39b543
commit
0045e532ee
3 changed files with 187 additions and 25 deletions
|
|
@ -7,6 +7,7 @@ from django.forms.models import ModelForm, ModelFormMetaclass
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from servala.core.models import ServiceInstance
|
||||
from servala.frontend.forms.widgets import DynamicArrayField, DynamicArrayWidget
|
||||
|
||||
|
||||
class CRDModel(models.Model):
|
||||
|
|
@ -22,17 +23,11 @@ class CRDModel(models.Model):
|
|||
|
||||
|
||||
def duplicate_field(field_name, model):
|
||||
# Get the field from the model
|
||||
field = model._meta.get_field(field_name)
|
||||
|
||||
# Create a new field with the same attributes
|
||||
new_field = type(field).__new__(type(field))
|
||||
new_field.__dict__.update(field.__dict__)
|
||||
|
||||
# Ensure the field is not linked to the original model
|
||||
new_field.model = None
|
||||
new_field.auto_created = False
|
||||
|
||||
return new_field
|
||||
|
||||
|
||||
|
|
@ -250,8 +245,6 @@ def get_django_field(
|
|||
)
|
||||
elif field_type == "array":
|
||||
kwargs["help_text"] = field_schema.get("description") or _("List of values")
|
||||
from servala.frontend.forms.widgets import DynamicArrayField
|
||||
|
||||
field = models.JSONField(**kwargs)
|
||||
formfield_kwargs = {
|
||||
"label": field.verbose_name,
|
||||
|
|
@ -291,7 +284,29 @@ def unnest_data(data):
|
|||
return result
|
||||
|
||||
|
||||
class CrdModelFormMixin:
|
||||
class FormGeneratorMixin:
|
||||
IS_CUSTOM_FORM = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for field in ("organization", "context"):
|
||||
if field in self.fields:
|
||||
self.fields[field].widget = forms.HiddenInput()
|
||||
|
||||
if self.instance and hasattr(self.instance, "name") and self.instance.name:
|
||||
if "name" in self.fields:
|
||||
self.fields["name"].disabled = True
|
||||
self.fields["name"].widget = forms.HiddenInput()
|
||||
|
||||
def has_mandatory_fields(self, field_list):
|
||||
for field_name in field_list:
|
||||
if field_name in self.fields and self.fields[field_name].required:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class CrdModelFormMixin(FormGeneratorMixin):
|
||||
HIDDEN_FIELDS = [
|
||||
"spec.compositeDeletePolicy",
|
||||
"spec.compositionRef",
|
||||
|
|
@ -317,9 +332,6 @@ class CrdModelFormMixin:
|
|||
super().__init__(*args, **kwargs)
|
||||
self.schema = self._meta.model.SCHEMA
|
||||
|
||||
for field in ("organization", "context"):
|
||||
self.fields[field].widget = forms.HiddenInput()
|
||||
|
||||
for name, field in self.fields.items():
|
||||
if name in self.HIDDEN_FIELDS or any(
|
||||
name.startswith(f) for f in self.HIDDEN_FIELDS
|
||||
|
|
@ -327,22 +339,11 @@ class CrdModelFormMixin:
|
|||
field.widget = forms.HiddenInput()
|
||||
field.required = False
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["name"].disabled = True
|
||||
self.fields["name"].help_text = _("Name cannot be changed after creation.")
|
||||
self.fields["name"].widget = forms.HiddenInput()
|
||||
|
||||
def strip_title(self, field_name, label):
|
||||
field = self.fields[field_name]
|
||||
if field and field.label and (position := field.label.find(label)) != -1:
|
||||
field.label = field.label[position + len(label) :]
|
||||
|
||||
def has_mandatory_fields(self, field_list):
|
||||
for field_name in field_list:
|
||||
if field_name in self.fields and self.fields[field_name].required:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_fieldsets(self):
|
||||
fieldsets = []
|
||||
|
||||
|
|
@ -524,3 +525,142 @@ def generate_model_form_class(model):
|
|||
}
|
||||
class_name = f"{model.__name__}ModelForm"
|
||||
return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields)
|
||||
|
||||
|
||||
class CustomFormMixin(FormGeneratorMixin):
|
||||
"""
|
||||
Base for custom (user-friendly) forms generated from ServiceDefinition.form_config.
|
||||
"""
|
||||
|
||||
IS_CUSTOM_FORM = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._apply_field_config()
|
||||
if (
|
||||
self.instance
|
||||
and hasattr(self.instance, "name")
|
||||
and self.instance.name
|
||||
and "name" in self.fields
|
||||
):
|
||||
self.fields["name"].widget = forms.HiddenInput()
|
||||
self.fields["name"].disabled = True
|
||||
self.fields.pop("context", None)
|
||||
self.fields.pop("organization", None)
|
||||
|
||||
def _apply_field_config(self):
|
||||
for fieldset in self.form_config.get("fieldsets", []):
|
||||
for field_config in fieldset.get("fields", []):
|
||||
field_name = field_config.get("controlplane_field_mapping")
|
||||
|
||||
if field_name not in self.fields:
|
||||
continue
|
||||
|
||||
field = self.fields[field_name]
|
||||
field_type = field_config.get("type")
|
||||
|
||||
field.label = field_config.get("label", field_config["name"])
|
||||
field.help_text = field_config.get("help_text", "")
|
||||
field.required = field_config.get("required", False)
|
||||
|
||||
if field_type == "textarea":
|
||||
field.widget = forms.Textarea(
|
||||
attrs={"rows": field_config.get("rows", 4)}
|
||||
)
|
||||
elif field_type == "array":
|
||||
field.widget = DynamicArrayWidget()
|
||||
|
||||
if field_type == "number":
|
||||
min_val = field_config.get("min_value")
|
||||
max_val = field_config.get("max_value")
|
||||
|
||||
validators = []
|
||||
if min_val is not None:
|
||||
validators.append(MinValueValidator(min_val))
|
||||
if max_val is not None:
|
||||
validators.append(MaxValueValidator(max_val))
|
||||
|
||||
if validators:
|
||||
field.validators.extend(validators)
|
||||
|
||||
field.controlplane_field_mapping = field_name
|
||||
|
||||
def get_fieldsets(self):
|
||||
fieldsets = []
|
||||
for fieldset_config in self.form_config.get("fieldsets", []):
|
||||
field_names = [
|
||||
f["controlplane_field_mapping"]
|
||||
for f in fieldset_config.get("fields", [])
|
||||
]
|
||||
fieldset = {
|
||||
"title": fieldset_config.get("title", "General"),
|
||||
"fields": field_names,
|
||||
"fieldsets": [],
|
||||
"has_mandatory": self.has_mandatory_fields(field_names),
|
||||
}
|
||||
fieldsets.append(fieldset)
|
||||
|
||||
return fieldsets
|
||||
|
||||
def get_nested_data(self):
|
||||
nested = {}
|
||||
for field_name in self.fields.keys():
|
||||
if field_name in ("organization", "context"):
|
||||
value = self.cleaned_data.get(field_name)
|
||||
if value is not None:
|
||||
nested[field_name] = value
|
||||
continue
|
||||
|
||||
mapping = field_name
|
||||
value = self.cleaned_data.get(field_name)
|
||||
parts = mapping.split(".")
|
||||
current = nested
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
|
||||
current[parts[-1]] = value
|
||||
|
||||
return nested
|
||||
|
||||
|
||||
def generate_custom_form_class(form_config, model):
|
||||
"""
|
||||
Generate a custom (user-friendly) form class from form_config JSON.
|
||||
"""
|
||||
field_list = ["organization", "context", "name"]
|
||||
|
||||
for fieldset in form_config.get("fieldsets", []):
|
||||
for field_config in fieldset.get("fields", []):
|
||||
field_name = field_config.get("controlplane_field_mapping")
|
||||
if field_name:
|
||||
field_list.append(field_name)
|
||||
|
||||
fields = {
|
||||
"organization": forms.ModelChoiceField(
|
||||
queryset=None,
|
||||
required=True,
|
||||
widget=forms.HiddenInput(),
|
||||
),
|
||||
"context": forms.ModelChoiceField(
|
||||
queryset=None,
|
||||
required=True,
|
||||
widget=forms.HiddenInput(),
|
||||
),
|
||||
}
|
||||
|
||||
meta_attrs = {
|
||||
"model": model,
|
||||
"fields": field_list,
|
||||
}
|
||||
|
||||
form_fields = {
|
||||
"Meta": type("Meta", (object,), meta_attrs),
|
||||
"__module__": "crd_models",
|
||||
"form_config": form_config,
|
||||
**fields,
|
||||
}
|
||||
|
||||
class_name = f"{model.__name__}CustomForm"
|
||||
return ModelFormMetaclass(class_name, (CustomFormMixin, ModelForm), form_fields)
|
||||
|
|
|
|||
|
|
@ -162,10 +162,16 @@ class ServiceDefinitionAdminForm(forms.ModelForm):
|
|||
form_config = cleaned_data.get("form_config")
|
||||
if form_config:
|
||||
try:
|
||||
jsonschema.validate(instance=form_config, schema=self.form_config_schema)
|
||||
jsonschema.validate(
|
||||
instance=form_config, schema=self.form_config_schema
|
||||
)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise forms.ValidationError(
|
||||
{"form_config": _("Invalid form configuration: {}").format(e.message)}
|
||||
{
|
||||
"form_config": _("Invalid form configuration: {}").format(
|
||||
e.message
|
||||
)
|
||||
}
|
||||
)
|
||||
except jsonschema.SchemaError as e:
|
||||
raise forms.ValidationError(
|
||||
|
|
|
|||
|
|
@ -512,6 +512,22 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
|||
return
|
||||
return generate_model_form_class(self.django_model)
|
||||
|
||||
@cached_property
|
||||
def custom_model_form_class(self):
|
||||
from servala.core.crd import generate_custom_form_class
|
||||
|
||||
if not self.django_model:
|
||||
return
|
||||
if not (
|
||||
self.service_definition
|
||||
and self.service_definition.form_config
|
||||
and self.service_definition.form_config.get("fieldsets")
|
||||
):
|
||||
return
|
||||
return generate_custom_form_class(
|
||||
self.service_definition.form_config, self.django_model
|
||||
)
|
||||
|
||||
|
||||
class ServiceOffering(ServalaModelMixin, models.Model):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue