Improve settings hierarchy conciseness

This commit is contained in:
2026-03-07 16:32:24 +00:00
parent 611de57bf8
commit 10588a18b9
21 changed files with 846 additions and 80 deletions

View File

@@ -31,6 +31,7 @@ from core.models import (
PersonIdentifier,
QueuedMessage,
CommandSecurityPolicy,
UserAccessibilitySettings,
UserXmppOmemoState,
UserXmppSecuritySettings,
WorkspaceConversation,
@@ -489,6 +490,7 @@ class SecurityPage(LoginRequiredMixin, View):
"""Security settings page for OMEMO and command-scope policy controls."""
template_name = "pages/security.html"
page_mode = "encryption"
GLOBAL_SCOPE_KEY = "global.override"
# Allowed Services list used by both Global Scope Override and local scopes.
# Keep this in sync with the UI text on the Security page.
@@ -520,6 +522,18 @@ class SecurityPage(LoginRequiredMixin, View):
"other": "Other",
}
def _show_encryption(self) -> bool:
return str(getattr(self, "page_mode", "encryption")).strip().lower() in {
"encryption",
"all",
}
def _show_permission(self) -> bool:
return str(getattr(self, "page_mode", "encryption")).strip().lower() in {
"permission",
"all",
}
def _security_settings(self, request):
row, _ = UserXmppSecuritySettings.objects.get_or_create(user=request.user)
return row
@@ -616,6 +630,7 @@ class SecurityPage(LoginRequiredMixin, View):
def _scope_rows(self, request):
global_overrides = self._global_override_payload(request)["values"]
security_settings = self._security_settings(request)
rows = {
str(item.scope_key or "").strip().lower(): item
for item in CommandSecurityPolicy.objects.filter(user=request.user).exclude(
@@ -637,7 +652,10 @@ class SecurityPage(LoginRequiredMixin, View):
if not channel_rules:
channel_rules = [{"service": "xmpp", "pattern": ""}]
enabled_locked = global_overrides["scope_enabled"] != "per_scope"
require_omemo_locked = global_overrides["require_omemo"] != "per_scope"
require_omemo_locked = (
global_overrides["require_omemo"] != "per_scope"
or bool(security_settings.require_omemo)
)
require_trusted_locked = (
global_overrides["require_trusted_fingerprint"] != "per_scope"
)
@@ -661,6 +679,11 @@ class SecurityPage(LoginRequiredMixin, View):
"require_omemo_locked": require_omemo_locked,
"require_trusted_fingerprint_locked": require_trusted_locked,
"lock_help": "Set this field to 'Per Scope' in Global Scope Override to edit it here.",
"require_omemo_lock_help": (
"Disable 'Require OMEMO encryption' in Encryption settings to edit this field."
if bool(security_settings.require_omemo)
else "Set this field to 'Per Scope' in Global Scope Override to edit it here."
),
"allowed_services": raw_allowed_services,
"channel_rules": channel_rules,
})
@@ -668,6 +691,8 @@ class SecurityPage(LoginRequiredMixin, View):
def _scope_group_key(self, scope_key: str) -> str:
key = str(scope_key or "").strip().lower()
if key in {"tasks.commands", "gateway.tasks"}:
return "tasks"
if key in {"command.codex", "command.claude"}:
return "agentic"
if key.startswith("gateway."):
@@ -712,12 +737,17 @@ class SecurityPage(LoginRequiredMixin, View):
if "require_omemo" in request.POST:
row.require_omemo = _to_bool(request.POST.get("require_omemo"), False)
row.save(update_fields=["require_omemo", "updated_at"])
redirect_to = HttpResponseRedirect(reverse("security_settings"))
redirect_to = HttpResponseRedirect(request.path)
scope_key = str(request.POST.get("scope_key") or "").strip().lower()
if not self._show_permission():
return redirect_to
if scope_key == self.GLOBAL_SCOPE_KEY:
global_row = self._global_override_payload(request)["row"]
security_settings = self._security_settings(request)
settings_payload = dict(global_row.settings or {})
for field in self.GLOBAL_OVERRIDE_FIELDS:
if field == "require_omemo" and bool(security_settings.require_omemo):
continue
settings_payload[field] = self._parse_override_value(
request.POST.get(f"global_{field}")
)
@@ -742,6 +772,7 @@ class SecurityPage(LoginRequiredMixin, View):
if str(request.POST.get("scope_change_mode") or "").strip() != "1":
return redirect_to
global_overrides = self._global_override_payload(request)["values"]
security_settings = self._security_settings(request)
allowed_services = [
str(item or "").strip().lower()
for item in request.POST.getlist("allowed_services")
@@ -756,7 +787,10 @@ class SecurityPage(LoginRequiredMixin, View):
policy.allowed_channels = allowed_channels
if global_overrides["scope_enabled"] == "per_scope":
policy.enabled = _to_bool(request.POST.get("policy_enabled"), True)
if global_overrides["require_omemo"] == "per_scope":
if (
global_overrides["require_omemo"] == "per_scope"
and not bool(security_settings.require_omemo)
):
policy.require_omemo = _to_bool(
request.POST.get("policy_require_omemo"), False
)
@@ -778,35 +812,41 @@ class SecurityPage(LoginRequiredMixin, View):
return redirect_to
def get(self, request):
xmpp_state = transport.get_runtime_state("xmpp")
try:
omemo_row = UserXmppOmemoState.objects.get(user=request.user)
except UserXmppOmemoState.DoesNotExist:
omemo_row = None
show_encryption = self._show_encryption()
show_permission = self._show_permission()
xmpp_state = transport.get_runtime_state("xmpp") if show_encryption else {}
omemo_row = None
if show_encryption:
try:
omemo_row = UserXmppOmemoState.objects.get(user=request.user)
except UserXmppOmemoState.DoesNotExist:
omemo_row = None
security_settings = self._security_settings(request)
sender_jid = _parse_xmpp_jid(getattr(omemo_row, "last_sender_jid", "") or "")
omemo_plan = [
{
"label": "Component OMEMO active",
"done": bool(xmpp_state.get("omemo_enabled")),
"hint": "The gateway's OMEMO plugin must be loaded and initialised.",
},
{
"label": "OMEMO observed from your client",
"done": omemo_row is not None and omemo_row.status == "detected",
"hint": "Send any message with OMEMO enabled in your XMPP client.",
},
{
"label": "Client key on file",
"done": bool(getattr(omemo_row, "latest_client_key", "")),
"hint": "A device key (sid/rid) must be recorded from your client.",
},
{
"label": "Encryption required",
"done": security_settings.require_omemo,
"hint": "Enable 'Require OMEMO encryption' in Security Policy above to enforce this policy.",
},
]
omemo_plan = []
if show_encryption:
omemo_plan = [
{
"label": "Component OMEMO active",
"done": bool(xmpp_state.get("omemo_enabled")),
"hint": "The gateway's OMEMO plugin must be loaded and initialised.",
},
{
"label": "OMEMO observed from your client",
"done": omemo_row is not None and omemo_row.status == "detected",
"hint": "Send any message with OMEMO enabled in your XMPP client.",
},
{
"label": "Client key on file",
"done": bool(getattr(omemo_row, "latest_client_key", "")),
"hint": "A device key (sid/rid) must be recorded from your client.",
},
{
"label": "Encryption required",
"done": security_settings.require_omemo,
"hint": "Enable 'Require OMEMO encryption' in Security Policy above to enforce this policy.",
},
]
return render(request, self.template_name, {
"xmpp_state": xmpp_state,
"omemo_row": omemo_row,
@@ -817,4 +857,62 @@ class SecurityPage(LoginRequiredMixin, View):
"policy_groups": self._grouped_scope_rows(request),
"sender_jid": sender_jid,
"omemo_plan": omemo_plan,
"show_encryption": show_encryption,
"show_permission": show_permission,
})
class AccessibilitySettings(LoginRequiredMixin, View):
template_name = "pages/accessibility-settings.html"
def _row(self, request):
row, _ = UserAccessibilitySettings.objects.get_or_create(user=request.user)
return row
def get(self, request):
return render(request, self.template_name, {
"accessibility_settings": self._row(request),
})
def post(self, request):
row = self._row(request)
row.disable_animations = _to_bool(request.POST.get("disable_animations"), False)
row.save(update_fields=["disable_animations", "updated_at"])
return HttpResponseRedirect(reverse("accessibility_settings"))
class _SettingsCategoryPage(LoginRequiredMixin, View):
template_name = "pages/settings-category.html"
category_key = "general"
category_title = "General"
category_description = ""
tabs = ()
def _tab_rows(self):
current_path = str(getattr(self.request, "path", "") or "")
rows = []
for label, href in self.tabs:
rows.append({
"label": label,
"href": href,
"active": current_path == href,
})
return rows
def get(self, request):
return render(request, self.template_name, {
"category_key": self.category_key,
"category_title": self.category_title,
"category_description": self.category_description,
"category_tabs": self._tab_rows(),
})
class AISettingsPage(LoginRequiredMixin, View):
def get(self, request):
return HttpResponseRedirect(reverse("ai_models"))
class ModulesSettingsPage(_SettingsCategoryPage):
def get(self, request):
return HttpResponseRedirect(reverse("command_routing"))