Compare commits
No commits in common. "a5d46b696f5b8a9aacecd277b1e9c397e28b9cb8" and "aa77a10de21aa761582182cb0d58c4555c0822b3" have entirely different histories.
a5d46b696f
...
aa77a10de2
4 changed files with 11 additions and 94 deletions
|
|
@ -6,7 +6,7 @@ from django.db import models
|
||||||
from django.forms.models import ModelForm, ModelFormMetaclass
|
from django.forms.models import ModelForm, ModelFormMetaclass
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from servala.core.models import ControlPlaneCRD, ServiceInstance
|
from servala.core.models import ServiceInstance, ControlPlaneCRD
|
||||||
from servala.frontend.forms.widgets import DynamicArrayField, DynamicArrayWidget
|
from servala.frontend.forms.widgets import DynamicArrayField, DynamicArrayWidget
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,80 +178,8 @@ class ServiceDefinitionAdminForm(forms.ModelForm):
|
||||||
{"form_config": _("Schema error: {}").format(e.message)}
|
{"form_config": _("Schema error: {}").format(e.message)}
|
||||||
)
|
)
|
||||||
|
|
||||||
self._validate_field_mappings(form_config, cleaned_data)
|
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
def _validate_field_mappings(self, form_config, cleaned_data):
|
|
||||||
if not self.instance.pk:
|
|
||||||
return
|
|
||||||
crd = self.instance.offering_control_planes.all().first()
|
|
||||||
if not crd:
|
|
||||||
return
|
|
||||||
|
|
||||||
schema = None
|
|
||||||
try:
|
|
||||||
schema = crd.resource_schema
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not schema or not (spec_schema := schema.get("properties", {}).get("spec")):
|
|
||||||
return
|
|
||||||
|
|
||||||
valid_paths = self._extract_field_paths(spec_schema, "spec") | {"name"}
|
|
||||||
included_mappings = set()
|
|
||||||
errors = []
|
|
||||||
for fieldset in form_config.get("fieldsets", []):
|
|
||||||
for field in fieldset.get("fields", []):
|
|
||||||
mapping = field.get("controlplane_field_mapping")
|
|
||||||
included_mappings.add(mapping)
|
|
||||||
if mapping and mapping not in valid_paths:
|
|
||||||
field_name = field.get("name", mapping)
|
|
||||||
errors.append(
|
|
||||||
_(
|
|
||||||
"Field '{}' has invalid mapping '{}'. Valid paths are: {}"
|
|
||||||
).format(
|
|
||||||
field_name,
|
|
||||||
mapping,
|
|
||||||
", ".join(sorted(valid_paths)[:10])
|
|
||||||
+ ("..." if len(valid_paths) > 10 else ""),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if "name" not in included_mappings:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
{
|
|
||||||
"form_config": _(
|
|
||||||
"You must include a `name` field in the custom form config."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
raise forms.ValidationError({"form_config": errors})
|
|
||||||
|
|
||||||
def _extract_field_paths(self, schema, prefix=""):
|
|
||||||
paths = set()
|
|
||||||
|
|
||||||
if not isinstance(schema, dict):
|
|
||||||
return paths
|
|
||||||
|
|
||||||
if "type" in schema and schema["type"] != "object":
|
|
||||||
if prefix:
|
|
||||||
paths.add(prefix)
|
|
||||||
|
|
||||||
if schema.get("properties"):
|
|
||||||
for prop_name, prop_schema in schema["properties"].items():
|
|
||||||
new_prefix = f"{prefix}.{prop_name}" if prefix else prop_name
|
|
||||||
paths.add(new_prefix)
|
|
||||||
paths.update(self._extract_field_paths(prop_schema, new_prefix))
|
|
||||||
|
|
||||||
if schema.get("type") == "array" and "items" in schema:
|
|
||||||
if prefix:
|
|
||||||
paths.add(prefix)
|
|
||||||
|
|
||||||
return paths
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.instance.api_definition = self.cleaned_data["api_definition"]
|
self.instance.api_definition = self.cleaned_data["api_definition"]
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from servala.core.models import Organization, OrganizationInvitation, Organizati
|
||||||
from servala.core.odoo import get_invoice_addresses, get_odoo_countries
|
from servala.core.odoo import get_invoice_addresses, get_odoo_countries
|
||||||
from servala.frontend.forms.mixins import HtmxMixin
|
from servala.frontend.forms.mixins import HtmxMixin
|
||||||
|
|
||||||
|
|
||||||
ORG_NAME_PATTERN = r"[\w\s\-.,&'()+]+"
|
ORG_NAME_PATTERN = r"[\w\s\-.,&'()+]+"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
def context_object(self):
|
def context_object(self):
|
||||||
if self.request.method == "POST":
|
if self.request.method == "POST":
|
||||||
return ControlPlaneCRD.objects.filter(
|
return ControlPlaneCRD.objects.filter(
|
||||||
pk=self.request.POST.get(
|
pk=self.request.POST.get("expert-context", self.request.POST.get("custom-context")),
|
||||||
"expert-context", self.request.POST.get("custom-context")
|
|
||||||
),
|
|
||||||
# Make sure we don’t use a malicious ID
|
# Make sure we don’t use a malicious ID
|
||||||
control_plane__in=self.planes,
|
control_plane__in=self.planes,
|
||||||
).first()
|
).first()
|
||||||
|
|
@ -133,27 +131,19 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
control_plane=self.selected_plane, service_offering=self.object
|
control_plane=self.selected_plane, service_offering=self.object
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
|
||||||
def get_instance_form_kwargs(self, ignore_data=False):
|
def get_instance_form_kwargs(self, ignore_data=False):
|
||||||
return {
|
return {"initial": {
|
||||||
"initial": {
|
|
||||||
"organization": self.request.organization,
|
"organization": self.request.organization,
|
||||||
"context": self.context_object,
|
"context": self.context_object,
|
||||||
},
|
}, "prefix": "expert", "data": self.request.POST if (self.request.method == "POST" and not ignore_data) else None
|
||||||
"prefix": "expert",
|
|
||||||
"data": (
|
|
||||||
self.request.POST
|
|
||||||
if (self.request.method == "POST" and not ignore_data)
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
||||||
return
|
return
|
||||||
|
|
||||||
return self.context_object.model_form_class(
|
return self.context_object.model_form_class(**self.get_instance_form_kwargs(ignore_data=ignore_data))
|
||||||
**self.get_instance_form_kwargs(ignore_data=ignore_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_custom_instance_form(self, ignore_data=False):
|
def get_custom_instance_form(self, ignore_data=False):
|
||||||
if not self.context_object or not self.context_object.custom_model_form_class:
|
if not self.context_object or not self.context_object.custom_model_form_class:
|
||||||
|
|
@ -179,9 +169,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
||||||
context["custom_service_form"] = self.get_custom_instance_form()
|
context["custom_service_form"] = self.get_custom_instance_form()
|
||||||
else:
|
else:
|
||||||
context["service_form"] = self.get_instance_form()
|
context["service_form"] = self.get_instance_form()
|
||||||
context["custom_service_form"] = self.get_custom_instance_form(
|
context["custom_service_form"] = self.get_custom_instance_form(ignore_data=True)
|
||||||
ignore_data=True
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
context["service_form"] = self.get_instance_form()
|
context["service_form"] = self.get_instance_form()
|
||||||
context["custom_service_form"] = self.get_custom_instance_form()
|
context["custom_service_form"] = self.get_custom_instance_form()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue