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(