forked from Fediversity/Fediversity
basic versioned forms
this is still rudimentary since the actual forms are not auto-generated from the underlying model. the comments show a path towards that.
This commit is contained in:
parent
981ba011ab
commit
9dd92b4cc1
6 changed files with 121 additions and 2 deletions
panel/src/panel
|
@ -5,14 +5,17 @@ import sys
|
|||
from importlib import import_module
|
||||
from importlib.util import find_spec
|
||||
from django.utils.functional import classproperty
|
||||
from django_pydantic_field import SchemaField
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django import forms
|
||||
|
||||
|
||||
class Version():
|
||||
|
||||
model: models.Model
|
||||
form: forms.ModelForm
|
||||
|
||||
@classproperty
|
||||
def latest(cls):
|
||||
|
@ -29,3 +32,5 @@ class Version():
|
|||
def __init__(self, version: int):
|
||||
module = import_module(f"{__name__}.v{version}")
|
||||
self.model = getattr(module, "Configuration")
|
||||
|
||||
self.form = getattr(module, "Form")
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from django import forms
|
||||
from django.db import models
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from django_pydantic_field import SchemaField
|
||||
|
||||
class Configuration(BaseModel):
|
||||
enable: bool = Field(
|
||||
|
@ -17,3 +20,25 @@ class Configuration(BaseModel):
|
|||
default=Domain.EU,
|
||||
description="DNS domain where to expose services"
|
||||
)
|
||||
|
||||
# TODO@(fricklerhandwerk):
|
||||
# generate this automatically from the Pydantic model so this code can go away:
|
||||
# - add custom types, splicing `forms.Form` into `pydantic.BaseModel` and `forms.Field` into `pydantic.Field` so we can attach Django validators to Pydantic models
|
||||
# - map primitive or well-known field types as captured by `pydantic.Field` to `forms.Field` constructors, and `BaseModel` to `Form` using `BaseModel.model_fields`
|
||||
# - use a custom widget that renders nested forms in e.g. a `<div>`
|
||||
# - likely this also needs overriding `as_p()`, `as_li()`
|
||||
# more inspiration: https://levelup.gitconnected.com/how-to-export-pydantic-models-as-django-forms-c1b59ddca580
|
||||
# that work goes through the JSON Schema generated by Pydantic, which seems unnecessary to me, but it follows the same princple
|
||||
# TODO(@fricklerhandwerk):
|
||||
# eventually we probably want to validate each field separately,
|
||||
# so we should also auto-generate views per field that will be called by htmx directives defined in the templates.
|
||||
# those htmx parts can be generated by each form field using `attrs`
|
||||
class Form(forms.Form):
|
||||
enable = forms.BooleanField(required=False)
|
||||
domain = forms.ChoiceField(choices=[(d.name, d.value) for d in Configuration.Domain])
|
||||
|
||||
def to_python(self):
|
||||
return Configuration(
|
||||
enable=self.cleaned_data['enable'],
|
||||
domain=Configuration.Domain[self.cleaned_data['domain']]
|
||||
)
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
<li>
|
||||
<a href="{% url 'service_list' %}">Services</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'configuration_form' %}">Configuration</a>
|
||||
</li>
|
||||
|
||||
{% load custom_tags %}
|
||||
<li>
|
||||
|
|
13
panel/src/panel/templates/configuration_form.html
Normal file
13
panel/src/panel/templates/configuration_form.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data" action="{% url 'configuration_form' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form.as_p }}
|
||||
|
||||
<button class="button" disabled>Deploy</button>
|
||||
<button class="button" type="submit" >Save</button>
|
||||
</form>
|
||||
|
||||
<p><sub>Configuration schema version {{ version }}</sub></p>
|
||||
{% endblock %}
|
|
@ -25,4 +25,5 @@ urlpatterns = [
|
|||
path("", include("django.contrib.auth.urls")),
|
||||
path("account/", views.AccountDetail.as_view(), name='account_detail'),
|
||||
path("services/", views.ServiceList.as_view(), name='service_list'),
|
||||
path("configuration/", views.ConfigurationForm.as_view(), name='configuration_form'),
|
||||
]
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
from enum import Enum
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic import DetailView
|
||||
from django.views.generic import TemplateView, DetailView
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from panel import models
|
||||
from panel.configuration import Version
|
||||
|
||||
class Index(TemplateView):
|
||||
template_name = 'index.html'
|
||||
|
@ -14,3 +20,69 @@ class AccountDetail(LoginRequiredMixin, DetailView):
|
|||
|
||||
class ServiceList(TemplateView):
|
||||
template_name = 'service_list.html'
|
||||
|
||||
class ConfigurationForm(LoginRequiredMixin, FormView):
|
||||
template_name = 'configuration_form.html'
|
||||
success_url = reverse_lazy('configuration_form')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["version"] = self.get_object().version
|
||||
return context
|
||||
|
||||
def get_form_class(self):
|
||||
config = self.get_object()
|
||||
return Version(config.version).form
|
||||
|
||||
def get_object(self):
|
||||
"""Get or create the configuration object for the current user"""
|
||||
obj, created = models.Configuration.objects.get_or_create(
|
||||
operator=self.request.user)
|
||||
return obj
|
||||
|
||||
# TODO(@fricklerhandwerk):
|
||||
# this should probably live somewhere else
|
||||
def convert_enums_to_names(self, data_dict):
|
||||
"""
|
||||
Recursively convert all enum values in a dictionary to their string names.
|
||||
This handles nested dictionaries and lists as well.
|
||||
|
||||
Needed for converting a Pydantic `BaseModel` instance to a `Form` input.
|
||||
"""
|
||||
if isinstance(data_dict, dict):
|
||||
result = {}
|
||||
for key, value in data_dict.items():
|
||||
if isinstance(value, Enum):
|
||||
# Convert Enum to its name
|
||||
result[key] = value.name
|
||||
elif isinstance(value, (dict, list)):
|
||||
# Recursively process nested structures
|
||||
result[key] = self.convert_enums_to_names(value)
|
||||
else:
|
||||
# Keep other values as is
|
||||
result[key] = value
|
||||
return result
|
||||
elif isinstance(data_dict, list):
|
||||
# Process each item in the list
|
||||
return [self.convert_enums_to_names(item) for item in data_dict]
|
||||
elif isinstance(data_dict, Enum):
|
||||
# Convert single Enum value
|
||||
return data_dict.name
|
||||
else:
|
||||
# Return non-dict, non-list, non-Enum values as is
|
||||
return data_dict
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
config = self.get_object()
|
||||
config_dict = config.parsed_value.model_dump()
|
||||
|
||||
initial.update(self.convert_enums_to_names(config_dict))
|
||||
return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
obj = self.get_object()
|
||||
obj.value = form.to_python().model_dump_json()
|
||||
obj.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
|
Loading…
Add table
Reference in a new issue