2026-05-19 12:13:12 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from django import forms
|
|
|
|
|
|
2026-05-19 12:17:17 +02:00
|
|
|
from .models import HostConfig, ScheduleConfig
|
2026-05-19 12:13:12 +02:00
|
|
|
from .scheduler import parse_cron_expr
|
|
|
|
|
|
|
|
|
|
|
2026-05-19 12:17:17 +02:00
|
|
|
class NewlineListField(forms.CharField):
|
|
|
|
|
widget = forms.Textarea
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs) -> None:
|
|
|
|
|
kwargs.setdefault("required", False)
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def prepare_value(self, value):
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
return "\n".join(str(item) for item in value)
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
def to_python(self, value) -> list[str]:
|
|
|
|
|
if not value:
|
|
|
|
|
return []
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
|
|
|
return [line.strip() for line in str(value).splitlines() if line.strip()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NullableNewlineListField(NewlineListField):
|
|
|
|
|
def to_python(self, value) -> list[str] | None:
|
|
|
|
|
parsed = super().to_python(value)
|
|
|
|
|
return parsed or None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HostConfigForm(forms.ModelForm):
|
|
|
|
|
includes = NewlineListField(help_text="One include path per line. Leave empty to include defaults.")
|
|
|
|
|
excludes_add = NewlineListField(help_text="One additional exclude pattern per line.")
|
|
|
|
|
excludes_replace = NullableNewlineListField(
|
|
|
|
|
help_text="Optional. When set, replaces global excludes; one pattern per line."
|
|
|
|
|
)
|
|
|
|
|
rsync_extra_args = NewlineListField(help_text="One extra rsync argument per line.")
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = HostConfig
|
|
|
|
|
fields = (
|
|
|
|
|
"address",
|
|
|
|
|
"enabled",
|
|
|
|
|
"ssh_user",
|
|
|
|
|
"ssh_port",
|
|
|
|
|
"source_root",
|
|
|
|
|
"includes",
|
|
|
|
|
"excludes_add",
|
|
|
|
|
"excludes_replace",
|
|
|
|
|
"rsync_extra_args",
|
|
|
|
|
"retention_daily",
|
|
|
|
|
"retention_weekly",
|
|
|
|
|
"retention_monthly",
|
|
|
|
|
"retention_yearly",
|
|
|
|
|
)
|
|
|
|
|
help_texts = {
|
|
|
|
|
"ssh_user": "Leave empty to use the global SSH user.",
|
|
|
|
|
"ssh_port": "Leave empty to use the global SSH port.",
|
|
|
|
|
"source_root": "Leave empty to use the global default source root.",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-05-19 12:13:12 +02:00
|
|
|
class ScheduleConfigForm(forms.ModelForm):
|
|
|
|
|
cron_expr = forms.CharField(
|
|
|
|
|
label="Cron expression",
|
|
|
|
|
help_text='Five-field cron expression, for example "15 2 * * *".',
|
|
|
|
|
)
|
|
|
|
|
prune_max_delete = forms.IntegerField(min_value=0)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = ScheduleConfig
|
|
|
|
|
fields = (
|
|
|
|
|
"cron_expr",
|
|
|
|
|
"user",
|
|
|
|
|
"enabled",
|
|
|
|
|
"prune",
|
|
|
|
|
"prune_max_delete",
|
|
|
|
|
"prune_protect_bases",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def clean_cron_expr(self) -> str:
|
|
|
|
|
cron_expr = self.cleaned_data["cron_expr"].strip()
|
|
|
|
|
try:
|
|
|
|
|
parse_cron_expr(cron_expr)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
raise forms.ValidationError(str(exc)) from exc
|
|
|
|
|
return cron_expr
|