forked from Fediversity/Fediversity
WIP: versioned configurations
TODO: - package https://github.com/surenkov/django-pydantic-field - manual tests and debugging - unit test (de-)serialisation
This commit is contained in:
parent
4db91bd0b7
commit
4435b99851
6 changed files with 163 additions and 0 deletions
panel/src/panel
0
panel/src/panel/configuration/__init__.py
Normal file
0
panel/src/panel/configuration/__init__.py
Normal file
18
panel/src/panel/configuration/base.py
Normal file
18
panel/src/panel/configuration/base.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.db import models
|
||||
import os
|
||||
import inspect
|
||||
|
||||
class VersionedConfiguration(models.Model):
|
||||
"""Base class for all configuration versions"""
|
||||
|
||||
class Meta:
|
||||
abstract = True # don't create a table for this model
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# get version from file name and set it on the model instance
|
||||
frame = inspect.stack()[1]
|
||||
module = inspect.getmodule(frame[0])
|
||||
module_file = os.path.basename(module.__file__)
|
||||
self.version = int(module_file.replace('v', '').split('.')[0])
|
30
panel/src/panel/configuration/v1.py
Normal file
30
panel/src/panel/configuration/v1.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django import forms
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, List, Optional
|
||||
from enum import Enum
|
||||
|
||||
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: package and use https://github.com/surenkov/django-pydantic-field for this to work
|
||||
class Form(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Configuration
|
||||
fields = [
|
||||
"enable"
|
||||
"domain"
|
||||
]
|
25
panel/src/panel/migrations/0001_initial.py
Normal file
25
panel/src/panel/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 4.2.16 on 2025-03-04 00:01
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import panel.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Configuration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('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)),
|
||||
('version', models.PositiveIntegerField(default=panel.models.latest_version, help_text='Configuration schema version')),
|
||||
('value', models.JSONField(help_text='Stored configuration value', null=True)),
|
||||
],
|
||||
),
|
||||
]
|
49
panel/src/panel/models.py
Normal file
49
panel/src/panel/models.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
import os
|
||||
import re
|
||||
import importlib.util
|
||||
import importlib
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
# TODO: add to factored-out dataclass
|
||||
def latest_version():
|
||||
# Get the directory where version modules are stored
|
||||
module_dir = os.path.dirname(importlib.util.find_spec('configuration').origin)
|
||||
|
||||
# Look for files matching v{number}.py
|
||||
version_pattern = re.compile(r'v(\d+)\.py$')
|
||||
versions = []
|
||||
for filename in os.listdir(module_dir):
|
||||
match = version_pattern.match(filename)
|
||||
if match:
|
||||
versions.append(int(match.group(1)))
|
||||
return max(versions)
|
||||
|
||||
|
||||
class Configuration(models.Model):
|
||||
operator = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="configurations",
|
||||
help_text="Operator who owns the configuration",
|
||||
)
|
||||
|
||||
version = models.PositiveIntegerField(
|
||||
help_text="Configuration schema version",
|
||||
default=latest_version,
|
||||
)
|
||||
|
||||
value = models.JSONField(
|
||||
help_text="Stored configuration value",
|
||||
# TODO: use the model's default value instead
|
||||
null=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def parsed_value(self):
|
||||
# TODO: use factored-out dataclass
|
||||
module = importlib.import_module(f"configuration.v{version}")
|
||||
config_class = getattr(module, "Configuration")
|
||||
return config_class.model_validate_json(self.value)
|
|
@ -1,16 +1,57 @@
|
|||
from importlib import import_module
|
||||
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.edit import UpdateView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from panel import models
|
||||
from panel 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, UpdateView):
|
||||
template_name = 'configuration_form.html'
|
||||
model = models.Configuration
|
||||
success_url = reverse_lazy('configuration_form')
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
obj, created = models.Configuration.objects.get_or_create(
|
||||
operator=self.request.user)
|
||||
return obj
|
||||
|
||||
def get_version_module(self):
|
||||
"""Helper method to get the version-specific module and config class"""
|
||||
config = self.get_object()
|
||||
# TODO: factor out a helper dataclass for versioned configurations featuring:
|
||||
# - latest available version
|
||||
# - schema(version):
|
||||
# - model
|
||||
# - form
|
||||
# - template
|
||||
module = import_module(f"configuration.v{config.version}")
|
||||
config_class = getattr(module, "Configuration")
|
||||
return module, config_class
|
||||
|
||||
def get_form_class(self):
|
||||
# Get module for this version
|
||||
# TODO: use the factored-out dataclass so we can do something like `self.get_version().form` instead
|
||||
module, _ = self.get_version_module()
|
||||
|
||||
# Return the ConfigurationForm class from that module
|
||||
return getattr(module, "Form")
|
||||
|
|
Loading…
Add table
Reference in a new issue