forked from Fediversity/Fediversity
Remove versions
This commit is contained in:
parent
e0053da5bb
commit
3f306a7199
8 changed files with 49 additions and 137 deletions
panel/src/panel
|
@ -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")
|
|
@ -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']),
|
||||
)
|
|
@ -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']]
|
||||
)
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue