Add start of a settings (config) system
This commit is contained in:
		
							parent
							
								
									495e955378
								
							
						
					
					
						commit
						44af0d4c59
					
				
							
								
								
									
										8
									
								
								core/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								core/admin.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from core.models import Config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Config)
 | 
			
		||||
class ConfigAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ["id", "key", "user", "identity"]
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
import pydantic
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Config(pydantic.BaseModel):
 | 
			
		||||
 | 
			
		||||
    # Basic configuration options
 | 
			
		||||
    site_name: str = "takahē"
 | 
			
		||||
    identity_max_age: int = 24 * 60 * 60
 | 
			
		||||
 | 
			
		||||
    # Cached ORM object storage
 | 
			
		||||
    __singleton__ = None
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        env_prefix = "takahe_"
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def load(cls) -> "Config":
 | 
			
		||||
        if cls.__singleton__ is None:
 | 
			
		||||
            cls.__singleton__ = cls()
 | 
			
		||||
        return cls.__singleton__
 | 
			
		||||
@ -1,7 +1,10 @@
 | 
			
		||||
from core.config import Config
 | 
			
		||||
from core.models import Config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def config_context(request):
 | 
			
		||||
    return {
 | 
			
		||||
        "config": Config.load(),
 | 
			
		||||
        "config": Config.load_system(),
 | 
			
		||||
        "config_identity": (
 | 
			
		||||
            Config.load_identity(request.identity) if request.identity else None
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										63
									
								
								core/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								core/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
			
		||||
# Generated by Django 4.1.3 on 2022-11-16 21:23
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("users", "0002_identity_public_key_id"),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="Config",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "id",
 | 
			
		||||
                    models.BigAutoField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        verbose_name="ID",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("key", models.CharField(max_length=500)),
 | 
			
		||||
                ("json", models.JSONField(blank=True, null=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "image",
 | 
			
		||||
                    models.ImageField(
 | 
			
		||||
                        blank=True, null=True, upload_to="config/%Y/%m/%d/"
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "identity",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="configs",
 | 
			
		||||
                        to="users.identity",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "user",
 | 
			
		||||
                    models.ForeignKey(
 | 
			
		||||
                        blank=True,
 | 
			
		||||
                        null=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        related_name="configs",
 | 
			
		||||
                        to=settings.AUTH_USER_MODEL,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "unique_together": {("key", "user", "identity")},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								core/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								core/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										1
									
								
								core/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								core/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
from .config import Config  # noqa
 | 
			
		||||
							
								
								
									
										111
									
								
								core/models/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								core/models/config.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,111 @@
 | 
			
		||||
from typing import ClassVar
 | 
			
		||||
 | 
			
		||||
import pydantic
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.functional import classproperty
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Config(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    A configuration setting for either the server or a specific user or identity.
 | 
			
		||||
 | 
			
		||||
    The possible options and their defaults are defined at the bottom of the file.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    key = models.CharField(max_length=500)
 | 
			
		||||
 | 
			
		||||
    user = models.ForeignKey(
 | 
			
		||||
        "users.user",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
        related_name="configs",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    identity = models.ForeignKey(
 | 
			
		||||
        "users.identity",
 | 
			
		||||
        blank=True,
 | 
			
		||||
        null=True,
 | 
			
		||||
        related_name="configs",
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    json = models.JSONField(blank=True, null=True)
 | 
			
		||||
    image = models.ImageField(blank=True, null=True, upload_to="config/%Y/%m/%d/")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        unique_together = [
 | 
			
		||||
            ("key", "user", "identity"),
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @classproperty
 | 
			
		||||
    def system(cls):
 | 
			
		||||
        cls.system = cls.load_system()
 | 
			
		||||
        return cls.system
 | 
			
		||||
 | 
			
		||||
    system: ClassVar["Config.ConfigOptions"]  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def load_system(cls):
 | 
			
		||||
        """
 | 
			
		||||
        Load all of the system config options and return an object with them
 | 
			
		||||
        """
 | 
			
		||||
        values = {}
 | 
			
		||||
        for config in cls.objects.filter(user__isnull=True, identity__isnull=True):
 | 
			
		||||
            values[config.key] = config.image or config.json
 | 
			
		||||
        return cls.SystemOptions(**values)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def load_user(cls, user):
 | 
			
		||||
        """
 | 
			
		||||
        Load all of the user config options and return an object with them
 | 
			
		||||
        """
 | 
			
		||||
        values = {}
 | 
			
		||||
        for config in cls.objects.filter(user=user, identity__isnull=True):
 | 
			
		||||
            values[config.key] = config.image or config.json
 | 
			
		||||
        return cls.UserOptions(**values)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def load_identity(cls, identity):
 | 
			
		||||
        """
 | 
			
		||||
        Load all of the identity config options and return an object with them
 | 
			
		||||
        """
 | 
			
		||||
        values = {}
 | 
			
		||||
        for config in cls.objects.filter(user__isnull=True, identity=identity):
 | 
			
		||||
            values[config.key] = config.image or config.json
 | 
			
		||||
        return cls.IdentityOptions(**values)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def set_system(cls, key, value):
 | 
			
		||||
        config_field = cls.SystemOptions.__fields__[key]
 | 
			
		||||
        if not isinstance(value, config_field.type_):
 | 
			
		||||
            raise ValueError(f"Invalid type for {key}: {type(value)}")
 | 
			
		||||
        cls.objects.update_or_create(
 | 
			
		||||
            key=key,
 | 
			
		||||
            defaults={"json": value},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def set_identity(cls, identity, key, value):
 | 
			
		||||
        config_field = cls.IdentityOptions.__fields__[key]
 | 
			
		||||
        if not isinstance(value, config_field.type_):
 | 
			
		||||
            raise ValueError(f"Invalid type for {key}: {type(value)}")
 | 
			
		||||
        cls.objects.update_or_create(
 | 
			
		||||
            identity=identity,
 | 
			
		||||
            key=key,
 | 
			
		||||
            defaults={"json": value},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class SystemOptions(pydantic.BaseModel):
 | 
			
		||||
 | 
			
		||||
        site_name: str = "takahē"
 | 
			
		||||
        highlight_color: str = "#449c8c"
 | 
			
		||||
        identity_max_age: int = 24 * 60 * 60
 | 
			
		||||
 | 
			
		||||
    class UserOptions(pydantic.BaseModel):
 | 
			
		||||
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    class IdentityOptions(pydantic.BaseModel):
 | 
			
		||||
 | 
			
		||||
        toot_mode: bool = False
 | 
			
		||||
@ -163,6 +163,8 @@ header menu a.identity {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
header menu a i {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -604,3 +606,19 @@ h1.identity small {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@media (max-width: 800px) {
 | 
			
		||||
    header menu a {
 | 
			
		||||
        font-size: 0;
 | 
			
		||||
        padding: 10px 20px 4px 20px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    header menu a i {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        vertical-align: middle;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        font-size: 20px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ from django.urls import path
 | 
			
		||||
from activities.views import posts, timelines
 | 
			
		||||
from core import views as core
 | 
			
		||||
from stator import views as stator
 | 
			
		||||
from users.views import activitypub, auth, identity
 | 
			
		||||
from users.views import activitypub, auth, identity, settings_identity, settings_system
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path("", core.homepage),
 | 
			
		||||
@ -13,6 +13,10 @@ urlpatterns = [
 | 
			
		||||
    path("notifications/", timelines.Notifications.as_view()),
 | 
			
		||||
    path("local/", timelines.Local.as_view()),
 | 
			
		||||
    path("federated/", timelines.Federated.as_view()),
 | 
			
		||||
    path("settings/", settings_identity.IdentitySettingsRoot.as_view()),
 | 
			
		||||
    path("settings/interface/", settings_identity.InterfacePage.as_view()),
 | 
			
		||||
    path("settings/system/", settings_system.SystemSettingsRoot.as_view()),
 | 
			
		||||
    path("settings/system/basic/", settings_system.BasicPage.as_view()),
 | 
			
		||||
    # Identity views
 | 
			
		||||
    path("@<handle>/", identity.ViewIdentity.as_view()),
 | 
			
		||||
    path("@<handle>/actor/", activitypub.Actor.as_view()),
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@
 | 
			
		||||
            {% include "forms/_field.html" %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <button>Post</button>
 | 
			
		||||
            <button>{% if config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@
 | 
			
		||||
                {{ form.content_warning }}
 | 
			
		||||
                <div class="buttons">
 | 
			
		||||
                    <span class="button toggle" _="on click toggle .enabled then toggle .hidden on #id_content_warning">CW</span>
 | 
			
		||||
                    <button>Post</button>
 | 
			
		||||
                    <button>{% if config_identity.toot_mode %}Toot!{% else %}Post{% endif %}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,11 @@
 | 
			
		||||
    <link rel="manifest" href="/manifest.json" />
 | 
			
		||||
    <script src="{% static "js/hyperscript.min.js" %}"></script>
 | 
			
		||||
    <script src="{% static "js/htmx.min.js" %}"></script>
 | 
			
		||||
    <style>
 | 
			
		||||
        body {
 | 
			
		||||
            --color-highlight: {{ config.highlight_color }};
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
    {% block extra_head %}{% endblock %}
 | 
			
		||||
</head>
 | 
			
		||||
<body class="{% block body_class %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
 | 
			
		||||
@ -23,8 +28,11 @@
 | 
			
		||||
            </a>
 | 
			
		||||
            <menu>
 | 
			
		||||
                {% if user.is_authenticated %}
 | 
			
		||||
                    <a href="/compose/"><i class="fa-solid fa-feather"></i> Compose</a>
 | 
			
		||||
                    <a href="/settings/"><i class="fa-solid fa-gear"></i> Settings</a>
 | 
			
		||||
                    <a href="/compose/" title="Compose"><i class="fa-solid fa-feather"></i> Compose</a>
 | 
			
		||||
                    <a href="/settings/" title="Settings"><i class="fa-solid fa-gear"></i> Settings</a>
 | 
			
		||||
                    {% if request.user.admin %}
 | 
			
		||||
                        <a href="/settings/system/" title="Admin"><i class="fa-solid fa-toolbox"></i> Admin</a>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <div class="gap"></div>
 | 
			
		||||
                    <a href="/identity/select/" class="identity">
 | 
			
		||||
                        {% if not request.identity %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								templates/settings/_settings_identity_menu.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								templates/settings/_settings_identity_menu.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
<nav>
 | 
			
		||||
    <a href="#" {% if section == "profile" %}class="selected"{% endif %}>Profile</a>
 | 
			
		||||
    <a href="#" {% if section == "interface" %}class="selected"{% endif %}>Interface</a>
 | 
			
		||||
    <a href="#" {% if section == "filtering" %}class="selected"{% endif %}>Filtering</a>
 | 
			
		||||
</nav>
 | 
			
		||||
							
								
								
									
										3
									
								
								templates/settings/_settings_system_menu.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								templates/settings/_settings_system_menu.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
<nav>
 | 
			
		||||
    <a href="#" {% if section == "basic" %}class="selected"{% endif %}>Basic</a>
 | 
			
		||||
</nav>
 | 
			
		||||
							
								
								
									
										7
									
								
								templates/settings/settings_identity.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								templates/settings/settings_identity.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
{% extends "settings/settings_system.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ section.title }} - Settings{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block menu %}
 | 
			
		||||
    {% include "settings/_settings_identity_menu.html" %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										18
									
								
								templates/settings/settings_system.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								templates/settings/settings_system.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ section.title }} - System Settings{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    {% block menu %}
 | 
			
		||||
        {% include "settings/_settings_system_menu.html" %}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
    <form action="." method="POST">
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
        {% for field in form %}
 | 
			
		||||
            {% include "forms/_field.html" %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
            <button>Save</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -7,7 +7,7 @@ from django.shortcuts import redirect
 | 
			
		||||
from django.utils.decorators import method_decorator
 | 
			
		||||
from django.views.generic import FormView, TemplateView, View
 | 
			
		||||
 | 
			
		||||
from core.config import Config
 | 
			
		||||
from core.models import Config
 | 
			
		||||
from users.decorators import identity_required
 | 
			
		||||
from users.models import Domain, Follow, Identity, IdentityStates
 | 
			
		||||
from users.shortcuts import by_handle_or_404
 | 
			
		||||
@ -25,7 +25,7 @@ class ViewIdentity(TemplateView):
 | 
			
		||||
            fetch=True,
 | 
			
		||||
        )
 | 
			
		||||
        posts = identity.posts.all()[:100]
 | 
			
		||||
        if identity.data_age > Config.load().identity_max_age:
 | 
			
		||||
        if identity.data_age > Config.system.identity_max_age:
 | 
			
		||||
            identity.transition_perform(IdentityStates.outdated)
 | 
			
		||||
        return {
 | 
			
		||||
            "identity": identity,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								users/views/settings_identity.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								users/views/settings_identity.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
from django.utils.decorators import method_decorator
 | 
			
		||||
from django.views.generic import RedirectView
 | 
			
		||||
 | 
			
		||||
from core.models import Config
 | 
			
		||||
from users.decorators import identity_required
 | 
			
		||||
from users.views.settings_system import SystemSettingsPage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(identity_required, name="dispatch")
 | 
			
		||||
class IdentitySettingsRoot(RedirectView):
 | 
			
		||||
    url = "/settings/interface/"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IdentitySettingsPage(SystemSettingsPage):
 | 
			
		||||
    """
 | 
			
		||||
    Shows a settings page dynamically created from our settings layout
 | 
			
		||||
    at the bottom of the page. Don't add this to a URL directly - subclass!
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    options_class = Config.IdentityOptions
 | 
			
		||||
    template_name = "settings/settings_identity.html"
 | 
			
		||||
 | 
			
		||||
    def load_config(self):
 | 
			
		||||
        return Config.load_identity(self.request.identity)
 | 
			
		||||
 | 
			
		||||
    def save_config(self, key, value):
 | 
			
		||||
        Config.set_identity(self.request.identity, key, value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InterfacePage(IdentitySettingsPage):
 | 
			
		||||
 | 
			
		||||
    section = "interface"
 | 
			
		||||
 | 
			
		||||
    options = {
 | 
			
		||||
        "toot_mode": {
 | 
			
		||||
            "title": "I Will Toot As I Please",
 | 
			
		||||
            "help_text": "If enabled, changes all 'Post' buttons to 'Toot!'",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
							
								
								
									
										95
									
								
								users/views/settings_system.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								users/views/settings_system.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,95 @@
 | 
			
		||||
from functools import partial
 | 
			
		||||
from typing import ClassVar, Dict
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.shortcuts import redirect
 | 
			
		||||
from django.utils.decorators import method_decorator
 | 
			
		||||
from django.views.generic import FormView, RedirectView
 | 
			
		||||
 | 
			
		||||
from core.models import Config
 | 
			
		||||
from users.decorators import identity_required
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(identity_required, name="dispatch")
 | 
			
		||||
class SystemSettingsRoot(RedirectView):
 | 
			
		||||
    url = "/settings/system/basic/"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(identity_required, name="dispatch")
 | 
			
		||||
class SystemSettingsPage(FormView):
 | 
			
		||||
    """
 | 
			
		||||
    Shows a settings page dynamically created from our settings layout
 | 
			
		||||
    at the bottom of the page. Don't add this to a URL directly - subclass!
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    template_name = "settings/settings_system.html"
 | 
			
		||||
    options_class = Config.SystemOptions
 | 
			
		||||
    section: ClassVar[str]
 | 
			
		||||
    options: Dict[str, Dict[str, str]]
 | 
			
		||||
 | 
			
		||||
    def get_form_class(self):
 | 
			
		||||
        # Create the fields dict from the config object
 | 
			
		||||
        fields = {}
 | 
			
		||||
        for key, details in self.options.items():
 | 
			
		||||
            config_field = self.options_class.__fields__[key]
 | 
			
		||||
            if config_field.type_ is bool:
 | 
			
		||||
                form_field = partial(
 | 
			
		||||
                    forms.BooleanField,
 | 
			
		||||
                    widget=forms.Select(
 | 
			
		||||
                        choices=[(True, "Enabled"), (False, "Disabled")]
 | 
			
		||||
                    ),
 | 
			
		||||
                )
 | 
			
		||||
            elif config_field.type_ is str:
 | 
			
		||||
                form_field = forms.CharField
 | 
			
		||||
            else:
 | 
			
		||||
                raise ValueError(f"Cannot render settings type {config_field.type_}")
 | 
			
		||||
            fields[key] = form_field(
 | 
			
		||||
                label=details["title"],
 | 
			
		||||
                help_text=details.get("help_text", ""),
 | 
			
		||||
                required=details.get("required", False),
 | 
			
		||||
            )
 | 
			
		||||
        # Create a form class dynamically (yeah, right?) and return that
 | 
			
		||||
        return type("SettingsForm", (forms.Form,), fields)
 | 
			
		||||
 | 
			
		||||
    def load_config(self):
 | 
			
		||||
        return Config.load_system()
 | 
			
		||||
 | 
			
		||||
    def save_config(self, key, value):
 | 
			
		||||
        Config.set_system(key, value)
 | 
			
		||||
 | 
			
		||||
    def get_initial(self):
 | 
			
		||||
        config = self.load_config()
 | 
			
		||||
        initial = {}
 | 
			
		||||
        for key in self.options.keys():
 | 
			
		||||
            initial[key] = getattr(config, key)
 | 
			
		||||
        return initial
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self):
 | 
			
		||||
        context = super().get_context_data()
 | 
			
		||||
        context["section"] = self.section
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        # Save each key
 | 
			
		||||
        for field in form:
 | 
			
		||||
            self.save_config(
 | 
			
		||||
                field.name,
 | 
			
		||||
                form.cleaned_data[field.name],
 | 
			
		||||
            )
 | 
			
		||||
        return redirect(".")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasicPage(SystemSettingsPage):
 | 
			
		||||
 | 
			
		||||
    section = "basic"
 | 
			
		||||
 | 
			
		||||
    options = {
 | 
			
		||||
        "site_name": {
 | 
			
		||||
            "title": "Site Name",
 | 
			
		||||
            "help_text": "Shown in the top-left of the page, and titles",
 | 
			
		||||
        },
 | 
			
		||||
        "highlight_color": {
 | 
			
		||||
            "title": "Highlight Color",
 | 
			
		||||
            "help_text": "Used for logo background and other highlights",
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
		Reference in New Issue
	
	Block a user