Konrad du Plessis 3c28387dd3 WIP: 2026-04-22 session checkpoint
Complete working state of the session. Will be split into two deploy
phases (safety scaffolding then feature release) before merging to ai-dev.

Includes:
- Security fixes (email creds / SECRET_KEY / DEBUG / CSRF)
- Backup + restore management commands and browser endpoints
- WeasyPrint migration (replaces xhtml2pdf)
- New Worker fields + WorkerCertificate + WorkerWarning models
- Worker / Team / Project friendly management UIs
- Dashboard cert-expiry card + Manage All buttons
- Bootstrap tooltips (global init + theme-aware CSS)
- Django admin template override (taller M2M pickers)
- Money filter for ZAR currency formatting
- Resources dropdown nav
- Massive CLAUDE.md expansion + deploy plan docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:19:15 +02:00

133 lines
4.5 KiB
Python

# === BACKUP DATA MANAGEMENT COMMAND ===
# Exports every row of every core model to a single JSON file that can
# be restored later via `python manage.py restore_data <file.json>`.
#
# WHY THIS EXISTS:
# Flatlogic doesn't expose MySQL directly — no mysqldump, no SSH, no
# DB console. Django's built-in `dumpdata` / `loaddata` give us a
# platform-independent backup format that travels with the code.
#
# WHY NOT JUST USE `dumpdata`?
# This command is a thin wrapper around dumpdata that:
# - Pins the exact set of app+model rows we want to back up
# - Writes to a timestamped file so you never overwrite a backup
# - Includes Users + Groups + auth content types (so permissions
# restore correctly too)
# - Prints a row-count summary so you can confirm it worked
#
# USAGE (local):
# python manage.py backup_data → backups/foxlog_YYYYMMDD_HHMMSS.json
# python manage.py backup_data --output=my.json → my.json
#
# USAGE (Flatlogic, via browser):
# Visit /backup-data/ as admin — downloads the backup file to your browser.
import datetime
import io
import json
from pathlib import Path
from django.core import serializers
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType
from core.models import (
UserProfile, Project, Worker, Team, WorkLog,
PayrollRecord, Loan, PayrollAdjustment,
ExpenseReceipt, ExpenseLineItem,
WorkerCertificate, WorkerWarning,
)
# === BACKUP SCOPE ===
# The exact list of models we back up. Order matters for restore —
# we list models in dependency order (no FK should point at something
# that comes later in the list). Django's loaddata handles this
# correctly regardless, but keeping it sorted helps humans read it.
MODELS_TO_BACKUP = [
# Auth fundamentals — restore these first so FKs from UserProfile
# etc. find their user rows.
ContentType,
Permission,
Group,
User,
# Core app
UserProfile,
Project,
Worker,
Team,
WorkLog,
PayrollRecord,
Loan,
PayrollAdjustment,
ExpenseReceipt,
ExpenseLineItem,
WorkerCertificate,
WorkerWarning,
]
def build_backup_payload():
"""Return (json_str, summary_dict) for the current DB state.
Separated from the Command class so the browser view can reuse it
to stream the backup to the user's browser.
"""
# Pull every row of every model we care about, serialise as JSON.
# serializers.serialize("json", queryset) returns a JSON string.
# We concatenate by building one big list first, then dumping once.
all_rows = []
summary = {}
for model in MODELS_TO_BACKUP:
qs = list(model.objects.all())
summary[f"{model._meta.app_label}.{model._meta.model_name}"] = len(qs)
# Use the built-in Django serializer for proper natural-key support
serialized = serializers.serialize("python", qs)
all_rows.extend(serialized)
payload = {
"version": 1,
"exported_at": datetime.datetime.now().isoformat(),
"row_counts": summary,
"data": all_rows,
}
return json.dumps(payload, indent=2, default=str), summary
class Command(BaseCommand):
help = (
"Export every core-app row to a JSON file for backup/restore. "
"Writes to backups/foxlog_<timestamp>.json unless --output is given."
)
def add_arguments(self, parser):
parser.add_argument(
"--output",
type=str,
default=None,
help="Output filepath. Default: backups/foxlog_<timestamp>.json",
)
def handle(self, *args, **options):
json_str, summary = build_backup_payload()
# Default path: ./backups/foxlog_<timestamp>.json
if options["output"]:
output_path = Path(options["output"])
else:
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = Path("backups") / f"foxlog_{ts}.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json_str, encoding="utf-8")
# Print a summary so you can verify at a glance
self.stdout.write(self.style.SUCCESS(
f"Backup written to: {output_path}"
))
self.stdout.write(f"File size: {output_path.stat().st_size:,} bytes")
self.stdout.write("Row counts by model:")
for model_name, count in sorted(summary.items()):
self.stdout.write(f" {model_name:<40} {count:>6}")