# === 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 `. # # 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_.json unless --output is given." ) def add_arguments(self, parser): parser.add_argument( "--output", type=str, default=None, help="Output filepath. Default: backups/foxlog_.json", ) def handle(self, *args, **options): json_str, summary = build_backup_payload() # Default path: ./backups/foxlog_.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}")