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>
133 lines
4.5 KiB
Python
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}")
|