forked from Fediversity/Fediversity
Remove versions
This commit is contained in:
parent
e41f9c572a
commit
08d109cc82
8 changed files with 49 additions and 137 deletions
|
@ -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):
|
# TODO(@fricklerhandwerk):
|
||||||
# Eventually should probably maintain a separate series of configuration schema versions for each service.
|
# 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.
|
# I didn't start it here yet, mainly to keep it readable.
|
||||||
|
|
||||||
|
|
||||||
class PeerTube(BaseModel):
|
class PeerTube(BaseModel):
|
||||||
enable: bool = Field(
|
enable: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Enable PeerTube",
|
description="Enable PeerTube",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Pixelfed(BaseModel):
|
class Pixelfed(BaseModel):
|
||||||
enable: bool = Field(
|
enable: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Enable Pixelfed",
|
description="Enable Pixelfed",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Mastodon(BaseModel):
|
class Mastodon(BaseModel):
|
||||||
enable: bool = Field(
|
enable: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Enable Mastodon",
|
description="Enable Mastodon",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Configuration(BaseModel):
|
class Configuration(BaseModel):
|
||||||
enable: bool = Field(
|
enable: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -61,7 +66,7 @@ class Configuration(BaseModel):
|
||||||
# generate this automatically from the Pydantic model so this code can go away:
|
# 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
|
# - 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`
|
# - 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()`
|
# - likely this also needs overriding `as_p()`, `as_li()`
|
||||||
# more inspiration: https://levelup.gitconnected.com/how-to-export-pydantic-models-as-django-forms-c1b59ddca580
|
# 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
|
# 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,
|
# 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.
|
# 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`
|
# those htmx parts can be generated by each form field using `attrs`
|
||||||
|
|
||||||
|
|
||||||
class Form(forms.Form):
|
class Form(forms.Form):
|
||||||
enable = forms.BooleanField(required=False)
|
enable = forms.BooleanField(required=False)
|
||||||
domain = forms.ChoiceField(
|
domain = forms.ChoiceField(
|
||||||
|
@ -101,7 +108,6 @@ class Form(forms.Form):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def to_python(self):
|
def to_python(self):
|
||||||
return Configuration(
|
return Configuration(
|
||||||
enable=self.cleaned_data['enable'],
|
enable=self.cleaned_data['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(
|
migrations.CreateModel(
|
||||||
name='Configuration',
|
name='Configuration',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True,
|
||||||
('version', models.PositiveIntegerField(default=1, help_text='Configuration schema version')),
|
primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('value', models.JSONField(help_text='Stored configuration value', null=True)),
|
('value', models.JSONField(
|
||||||
('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)),
|
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.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import pydantic.main
|
from panel.configuration.forms import Configuration
|
||||||
|
|
||||||
from panel.configuration import Version
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
@ -17,11 +16,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='configuration',
|
model_name='configuration',
|
||||||
name='value',
|
name='value',
|
||||||
field=models.JSONField(default=Version(Version.latest).model().model_dump_json, help_text='Stored configuration value'),
|
field=models.JSONField(default=Configuration(
|
||||||
),
|
).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'),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from panel.configuration import Version
|
from panel.configuration import forms
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_config():
|
||||||
|
return forms.Configuration().model_dump_json()
|
||||||
|
|
||||||
|
|
||||||
class Configuration(models.Model):
|
class Configuration(models.Model):
|
||||||
operator = models.ForeignKey(
|
operator = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
|
@ -14,16 +19,11 @@ class Configuration(models.Model):
|
||||||
help_text="Operator who owns the configuration",
|
help_text="Operator who owns the configuration",
|
||||||
)
|
)
|
||||||
|
|
||||||
version = models.PositiveIntegerField(
|
|
||||||
help_text="Configuration schema version",
|
|
||||||
default=Version.latest,
|
|
||||||
)
|
|
||||||
|
|
||||||
value = models.JSONField(
|
value = models.JSONField(
|
||||||
help_text="Stored configuration value",
|
help_text="Stored configuration value",
|
||||||
default=Version(Version.latest).model().model_dump_json,
|
default=get_default_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parsed_value(self) -> BaseModel:
|
def parsed_value(self) -> BaseModel:
|
||||||
return Version(self.version).model.model_validate_json(self.value)
|
return forms.Configuration.model_validate_json(self.value)
|
||||||
|
|
|
@ -18,18 +18,12 @@ class ConfigurationForm(TestCase):
|
||||||
def test_configuration_form_submission(self):
|
def test_configuration_form_submission(self):
|
||||||
config = Configuration.objects.create(
|
config = Configuration.objects.create(
|
||||||
operator=self.user,
|
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.client.login(username=self.username, password=self.password)
|
self.client.login(username=self.username, password=self.password)
|
||||||
response = self.client.get(self.config_url)
|
response = self.client.get(self.config_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
context = response.context[0]
|
context = response.context[0]
|
||||||
|
|
||||||
# configuration should be disabled by default
|
# configuration should be disabled by default
|
||||||
|
|
|
@ -7,33 +7,34 @@ from django.views.generic import TemplateView, DetailView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
from panel import models
|
from panel import models
|
||||||
from panel.configuration import Version
|
from panel.configuration import forms
|
||||||
|
|
||||||
|
|
||||||
class Index(TemplateView):
|
class Index(TemplateView):
|
||||||
template_name = 'index.html'
|
template_name = 'index.html'
|
||||||
|
|
||||||
|
|
||||||
class AccountDetail(LoginRequiredMixin, DetailView):
|
class AccountDetail(LoginRequiredMixin, DetailView):
|
||||||
model = User
|
model = User
|
||||||
template_name = 'account_detail.html'
|
template_name = 'account_detail.html'
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
class ServiceList(TemplateView):
|
class ServiceList(TemplateView):
|
||||||
template_name = 'service_list.html'
|
template_name = 'service_list.html'
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationForm(LoginRequiredMixin, FormView):
|
class ConfigurationForm(LoginRequiredMixin, FormView):
|
||||||
template_name = 'configuration_form.html'
|
template_name = 'configuration_form.html'
|
||||||
success_url = reverse_lazy('configuration_form')
|
success_url = reverse_lazy('configuration_form')
|
||||||
|
form_class = forms.Form
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["version"] = self.get_object().version
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_form_class(self):
|
|
||||||
config = self.get_object()
|
|
||||||
return Version(config.version).form
|
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""Get or create the configuration object for the current user"""
|
"""Get or create the configuration object for the current user"""
|
||||||
obj, created = models.Configuration.objects.get_or_create(
|
obj, created = models.Configuration.objects.get_or_create(
|
||||||
|
|
Loading…
Add table
Reference in a new issue