1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/appsettings/views.py
catborise e04adec87b feat(logs): changes to users, groups, owners, flavors and settings are logged
The Logs page now records the panel's own records too: users (add,
change with the changed fields, delete, block, unblock, an admin's
password set, a user's own password change including the first-login
one, own profile edits), groups with their members and permissions,
VM owners from the accounts pages and from the VM page, OTP QR code
mails, flavors from web and API, app settings and the theme,
removed-VM records and SSH keys. No row carries a password or a key.

- A user is created in one transaction: when a VM named in
  NEW_USER_DEFAULT_INSTANCES is missing, no half-made user stays.
- The OTP mail checks the email address before it creates a device;
  without one it writes a failed row and makes no device.
- The VM page's owner rows read "VM web-1 owner bob: add" and
  "...: remove" instead of "Add owner: bob" and "Delete owner: 17"; a
  second owner refused by the single-owner setting is a failed row.
  Owner rows keep the VM's host and name, so the VM's log tab shows
  them.
- A setting or theme posted with its current value is no change and
  writes no row; the old free-text setting rows are gone.

Host-side objects (networks, storage, interfaces, secrets, filters)
follow in the next layer.
2026-10-23 11:10:24 +03:00

73 lines
3.1 KiB
Python

from pathlib import Path
from admin.decorators import superuser_only
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import gettext_noop as _
from logs.audit import audit
from operations.utils import BOUNDS
from appsettings.models import AppSettings
# the themes "manage.py build_themes" compiles; base.html links the chosen one
THEMES_DIR = Path(settings.BASE_DIR, "static", "css", "themes")
@superuser_only
def appsettings(request):
"""
:param request:
:return:
"""
bootstrap_theme = AppSettings.objects.get(key="BOOTSTRAP_THEME")
themes_list = sorted(p.name.removesuffix(".min.css") for p in THEMES_DIR.glob("*.min.css"))
# the theme has its own form; the other settings are listed one by one
appsettings = AppSettings.objects.exclude(
description__startswith="Bootstrap"
).order_by("name")
if request.method == "POST":
if "BOOTSTRAP_THEME" in request.POST:
theme = request.POST.get("BOOTSTRAP_THEME", "")
if theme not in themes_list:
messages.error(request, _("Unknown theme: %(theme)s") % {"theme": theme})
return HttpResponseRedirect(request.get_full_path())
if theme != bootstrap_theme.value:
with audit(request, None, "Setting Theme", f"change to {theme}"):
bootstrap_theme.value = theme
bootstrap_theme.save()
msg = _("Theme is changed. Now: %(theme)s") % {"theme": theme}
messages.success(request, msg)
return HttpResponseRedirect(request.get_full_path())
for setting in appsettings:
if setting.key in request.POST:
if setting.key in BOUNDS:
low, high, _default = BOUNDS[setting.key]
raw = request.POST.get(setting.key, "")
if not (raw.isascii() and raw.isdigit() and len(raw) <= 4 and low <= int(raw) <= high):
messages.error(request, _("%(setting)s must be a whole number between %(low)s and %(high)s")
% {"setting": setting.name, "low": low, "high": high})
return HttpResponseRedirect(request.get_full_path())
value = request.POST.get(setting.key, "")
try:
if value != setting.value:
with audit(request, None, f"Setting {setting.name}", f"change to {value}"):
setting.value = value
setting.save()
msg = _("%(setting)s is changed. Now: %(value)s") % {
"setting": setting.name,
"value": setting.value,
}
messages.success(request, msg)
except Exception as err:
msg = err
messages.error(request, msg)
return HttpResponseRedirect(request.get_full_path())
return render(request, "appsettings.html", locals())