From 3f306a719901beec0fa07d65bd6f13c63971f49e Mon Sep 17 00:00:00 2001 From: lois <lois@procolix.eu> Date: Thu, 13 Mar 2025 15:25:07 +0100 Subject: [PATCH] Remove versions --- panel/src/panel/configuration/__init__.py | 42 ------------------ .../panel/configuration/{v2.py => forms.py} | 40 ++++++++++------- panel/src/panel/configuration/v1.py | 44 ------------------- panel/src/panel/migrations/0001_initial.py | 10 +++-- ...0002_alter_configuration_value_and_more.py | 11 ++--- panel/src/panel/models.py | 16 +++---- .../panel/tests/test_configuration_form.py | 10 +---- panel/src/panel/views.py | 13 +++--- 8 files changed, 49 insertions(+), 137 deletions(-) rename panel/src/panel/configuration/{v2.py => forms.py} (82%) delete mode 100644 panel/src/panel/configuration/v1.py diff --git a/panel/src/panel/configuration/__init__.py b/panel/src/panel/configuration/__init__.py index 034b0298..e69de29b 100644 --- a/panel/src/panel/configuration/__init__.py +++ b/panel/src/panel/configuration/__init__.py @@ -1,42 +0,0 @@ -import os -import re -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 - - -# NOTE(@fricklerhandwerk): -# Never change historical configuration schema versions. -# (Changing helper and display code is okay though.) -# We're exercising handling versioning even if it's not required for production yet. -# Old versions can be deleted before prime time once we've established reliable patterns. - -class Version(): - - model: models.Model - form: forms.ModelForm - - @classproperty - def latest(cls): - current_dir = os.path.dirname(os.path.abspath(__file__)) - version_pattern = re.compile(r'v(\d+)\.py$') - versions = [] - for filename in os.listdir(current_dir): - match = version_pattern.match(filename) - if match: - versions.append(int(match.group(1))) - - return max(versions) - - def __init__(self, version: int): - module = import_module(f"{__name__}.v{version}") - self.model = getattr(module, "Configuration") - - self.form = getattr(module, "Form") diff --git a/panel/src/panel/configuration/v2.py b/panel/src/panel/configuration/forms.py similarity index 82% rename from panel/src/panel/configuration/v2.py rename to panel/src/panel/configuration/forms.py index eaaf972d..73216169 100644 --- a/panel/src/panel/configuration/v2.py +++ b/panel/src/panel/configuration/forms.py @@ -7,24 +7,29 @@ from django_pydantic_field import SchemaField # TODO(@fricklerhandwerk): # Eventually should probably maintain a separate series of configuration schema versions for each service. # I didn't start it here yet, mainly to keep it readable. + + class PeerTube(BaseModel): enable: bool = Field( - default=False, - description="Enable PeerTube", + default=False, + description="Enable PeerTube", ) + class Pixelfed(BaseModel): enable: bool = Field( - default=False, - description="Enable Pixelfed", + default=False, + description="Enable Pixelfed", ) + class Mastodon(BaseModel): enable: bool = Field( - default=False, - description="Enable Mastodon", + default=False, + description="Enable Mastodon", ) + class Configuration(BaseModel): enable: bool = Field( default=False, @@ -43,25 +48,25 @@ class Configuration(BaseModel): ) peertube: PeerTube = Field( - default=PeerTube(), - description="Configuration for PeerTube", + default=PeerTube(), + description="Configuration for PeerTube", ) pixelfed: Pixelfed = Field( - default=Pixelfed(), - description="Configuration for Pixelfed", + default=Pixelfed(), + description="Configuration for Pixelfed", ) mastodon: Mastodon = Field( - default=Mastodon(), - description="Configuration for Mastodon", + default=Mastodon(), + description="Configuration for Mastodon", ) # 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>` +# - use a custom'{"name": "John Doe", "age": 30 }' 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 @@ -69,6 +74,8 @@ class Configuration(BaseModel): # 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( @@ -101,12 +108,11 @@ class Form(forms.Form): super().__init__(*args, **kwargs) - def to_python(self): return Configuration( enable=self.cleaned_data['enable'], domain=Configuration.Domain[self.cleaned_data['domain']], - peertube = PeerTube(enable=self.cleaned_data['peertube_enable']), - pixelfed = Pixelfed(enable=self.cleaned_data['pixelfed_enable']), - mastodon = Mastodon(enable=self.cleaned_data['mastodon_enable']), + peertube=PeerTube(enable=self.cleaned_data['peertube_enable']), + pixelfed=Pixelfed(enable=self.cleaned_data['pixelfed_enable']), + mastodon=Mastodon(enable=self.cleaned_data['mastodon_enable']), ) diff --git a/panel/src/panel/configuration/v1.py b/panel/src/panel/configuration/v1.py deleted file mode 100644 index 15f2bbb8..00000000 --- a/panel/src/panel/configuration/v1.py +++ /dev/null @@ -1,44 +0,0 @@ -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( - default=False, - description="Enable the configuration", - ) - - # XXX: hard-code available apex domains for now, - # they will be prefixed by the user name - class Domain(Enum): - EU = "fediversity.eu" - NET = "fediversity.net" - - domain: Domain = Field( - 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']] - ) diff --git a/panel/src/panel/migrations/0001_initial.py b/panel/src/panel/migrations/0001_initial.py index 12fda70b..f6c3cc1d 100644 --- a/panel/src/panel/migrations/0001_initial.py +++ b/panel/src/panel/migrations/0001_initial.py @@ -17,10 +17,12 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Configuration', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.PositiveIntegerField(default=1, help_text='Configuration schema version')), - ('value', models.JSONField(help_text='Stored configuration value', null=True)), - ('operator', models.ForeignKey(help_text='Operator who owns the configuration', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='configurations', to=settings.AUTH_USER_MODEL)), + ('id', models.BigAutoField(auto_created=True, + primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.JSONField( + help_text='Stored configuration value', null=True)), + ('operator', models.ForeignKey(help_text='Operator who owns the configuration', null=True, + on_delete=django.db.models.deletion.SET_NULL, related_name='configurations', to=settings.AUTH_USER_MODEL)), ], ), ] diff --git a/panel/src/panel/migrations/0002_alter_configuration_value_and_more.py b/panel/src/panel/migrations/0002_alter_configuration_value_and_more.py index fbc95f83..77f90544 100644 --- a/panel/src/panel/migrations/0002_alter_configuration_value_and_more.py +++ b/panel/src/panel/migrations/0002_alter_configuration_value_and_more.py @@ -2,9 +2,8 @@ from django.conf import settings from django.db import migrations, models -import pydantic.main +from panel.configuration.forms import Configuration -from panel.configuration import Version class Migration(migrations.Migration): @@ -17,11 +16,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='configuration', name='value', - field=models.JSONField(default=Version(Version.latest).model().model_dump_json, help_text='Stored configuration value'), - ), - migrations.AlterField( - model_name='configuration', - name='version', - field=models.PositiveIntegerField(default=Version.latest, help_text='Configuration schema version'), + field=models.JSONField(default=Configuration( + ).model_dump_json(), help_text='Stored configuration value'), ), ] diff --git a/panel/src/panel/models.py b/panel/src/panel/models.py index aba43aa0..67426cb1 100644 --- a/panel/src/panel/models.py +++ b/panel/src/panel/models.py @@ -1,10 +1,15 @@ from django.db import models from django.contrib.auth.models import User -from panel.configuration import Version +from panel.configuration import forms from pydantic import BaseModel + +def get_default_config(): + return forms.Configuration().model_dump_json() + + class Configuration(models.Model): operator = models.ForeignKey( User, @@ -14,16 +19,11 @@ class Configuration(models.Model): help_text="Operator who owns the configuration", ) - version = models.PositiveIntegerField( - help_text="Configuration schema version", - default=Version.latest, - ) - value = models.JSONField( help_text="Stored configuration value", - default=Version(Version.latest).model().model_dump_json, + default=get_default_config, ) @property def parsed_value(self) -> BaseModel: - return Version(self.version).model.model_validate_json(self.value) + return forms.Configuration.model_validate_json(self.value) diff --git a/panel/src/panel/tests/test_configuration_form.py b/panel/src/panel/tests/test_configuration_form.py index ced65b93..a96d8441 100644 --- a/panel/src/panel/tests/test_configuration_form.py +++ b/panel/src/panel/tests/test_configuration_form.py @@ -17,19 +17,13 @@ class ConfigurationForm(TestCase): def test_configuration_form_submission(self): config = Configuration.objects.create( - operator = self.user, - # hard code the version so we can make some concrete assertions - # XXX(@fricklerhandwerk): - # As an exception, this test is allowed to change arbitrarily, - # exactly when we purge/squash development versions of the configuration schema. - version = 2, + operator=self.user, ) self.client.login(username=self.username, password=self.password) response = self.client.get(self.config_url) self.assertEqual(response.status_code, 200) - context = response.context[0] # configuration should be disabled by default @@ -39,7 +33,7 @@ class ConfigurationForm(TestCase): form_data = context['form'].initial.copy() form_data.update( - enable= True, + enable=True, mastodon_enable=True, ) diff --git a/panel/src/panel/views.py b/panel/src/panel/views.py index 535b9b96..13a8f80a 100644 --- a/panel/src/panel/views.py +++ b/panel/src/panel/views.py @@ -7,33 +7,34 @@ from django.views.generic import TemplateView, DetailView from django.views.generic.edit import FormView from panel import models -from panel.configuration import Version +from panel.configuration import forms + class Index(TemplateView): template_name = 'index.html' + class AccountDetail(LoginRequiredMixin, DetailView): model = User template_name = 'account_detail.html' + def get_object(self): return self.request.user + class ServiceList(TemplateView): template_name = 'service_list.html' + class ConfigurationForm(LoginRequiredMixin, FormView): template_name = 'configuration_form.html' success_url = reverse_lazy('configuration_form') + form_class = forms.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(