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
				
			
		|  | @ -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