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): | ||||
| #     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