Phase 6: Add data backup and cleanup management commands
- Created export_system_config.py command:
* Exports Plans, Credit Costs, AI Models, Industries, Sectors, etc.
* Saves to JSON files for V1.0 configuration backup
* Includes metadata with export timestamp and stats
* Usage: python manage.py export_system_config --output-dir=backups/config
- Created cleanup_user_data.py command:
* Safely deletes all user-generated data
* DRY-RUN mode to preview deletions
* Confirmation prompt for safety
* Production environment protection
* Deletes: Sites, Keywords, Content, Images, Transactions, Logs, etc.
* Preserves: System config and user accounts
* Usage: python manage.py cleanup_user_data --dry-run
python manage.py cleanup_user_data --confirm
Both commands essential for V1.0 pre-launch cleanup
This commit is contained in:
152
backend/igny8_core/management/commands/cleanup_user_data.py
Normal file
152
backend/igny8_core/management/commands/cleanup_user_data.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Management command to clean up all user-generated data (DESTRUCTIVE).
|
||||
This is used before V1.0 production launch to start with a clean database.
|
||||
|
||||
⚠️ WARNING: This permanently deletes ALL user data!
|
||||
|
||||
Usage:
|
||||
# DRY RUN (recommended first):
|
||||
python manage.py cleanup_user_data --dry-run
|
||||
|
||||
# ACTUAL CLEANUP (after reviewing dry-run):
|
||||
python manage.py cleanup_user_data --confirm
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Clean up all user-generated data (DESTRUCTIVE - for pre-launch cleanup)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--confirm',
|
||||
action='store_true',
|
||||
help='Confirm you want to delete all user data'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Show what would be deleted without actually deleting'
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not options['confirm'] and not options['dry_run']:
|
||||
self.stdout.write(
|
||||
self.style.ERROR('\n⚠️ ERROR: Must use --confirm or --dry-run flag\n')
|
||||
)
|
||||
self.stdout.write('Usage:')
|
||||
self.stdout.write(' python manage.py cleanup_user_data --dry-run # See what will be deleted')
|
||||
self.stdout.write(' python manage.py cleanup_user_data --confirm # Actually delete data\n')
|
||||
return
|
||||
|
||||
# Safety check: Prevent running in production unless explicitly allowed
|
||||
if getattr(settings, 'ENVIRONMENT', 'production') == 'production' and options['confirm']:
|
||||
self.stdout.write(
|
||||
self.style.ERROR('\n⚠️ BLOCKED: Cannot run cleanup in PRODUCTION environment!\n')
|
||||
)
|
||||
self.stdout.write('To allow this, temporarily set ENVIRONMENT to "staging" in settings.\n')
|
||||
return
|
||||
|
||||
# Import models
|
||||
from igny8_core.auth.models import Site, CustomUser
|
||||
from igny8_core.business.planning.models import Keywords, Clusters
|
||||
from igny8_core.business.content.models import ContentIdea, Tasks, Content, Images
|
||||
from igny8_core.modules.publisher.models import PublishingRecord
|
||||
from igny8_core.business.integration.models import WordPressSyncEvent
|
||||
from igny8_core.modules.billing.models import CreditTransaction, CreditUsageLog, Order
|
||||
from igny8_core.modules.system.models import Notification
|
||||
from igny8_core.modules.writer.models import AutomationRun
|
||||
|
||||
# Define models to clear (ORDER MATTERS - foreign keys)
|
||||
# Delete child records before parent records
|
||||
models_to_clear = [
|
||||
('Notifications', Notification),
|
||||
('Credit Usage Logs', CreditUsageLog),
|
||||
('Credit Transactions', CreditTransaction),
|
||||
('Orders', Order),
|
||||
('WordPress Sync Events', WordPressSyncEvent),
|
||||
('Publishing Records', PublishingRecord),
|
||||
('Automation Runs', AutomationRun),
|
||||
('Images', Images),
|
||||
('Content', Content),
|
||||
('Tasks', Tasks),
|
||||
('Content Ideas', ContentIdea),
|
||||
('Clusters', Clusters),
|
||||
('Keywords', Keywords),
|
||||
('Sites', Site), # Sites should be near last (many foreign keys)
|
||||
# Note: We do NOT delete CustomUser - keep admin users
|
||||
]
|
||||
|
||||
if options['dry_run']:
|
||||
self.stdout.write(self.style.WARNING('\n' + '=' * 70))
|
||||
self.stdout.write(self.style.WARNING('DRY RUN - No data will be deleted'))
|
||||
self.stdout.write(self.style.WARNING('=' * 70 + '\n'))
|
||||
|
||||
total_records = 0
|
||||
for name, model in models_to_clear:
|
||||
count = model.objects.count()
|
||||
total_records += count
|
||||
status = '✓' if count > 0 else '·'
|
||||
self.stdout.write(f' {status} Would delete {count:6d} {name}')
|
||||
|
||||
# Count users (not deleted)
|
||||
user_count = CustomUser.objects.count()
|
||||
self.stdout.write(f'\n → Keeping {user_count:6d} Users (not deleted)')
|
||||
|
||||
self.stdout.write(f'\n Total records to delete: {total_records:,}')
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS('\nTo proceed with actual deletion, run:'))
|
||||
self.stdout.write(' python manage.py cleanup_user_data --confirm\n')
|
||||
return
|
||||
|
||||
# ACTUAL DELETION
|
||||
self.stdout.write(self.style.ERROR('\n' + '=' * 70))
|
||||
self.stdout.write(self.style.ERROR('⚠️ DELETING ALL USER DATA - THIS CANNOT BE UNDONE!'))
|
||||
self.stdout.write(self.style.ERROR('=' * 70 + '\n'))
|
||||
|
||||
# Final confirmation prompt
|
||||
confirm_text = input('Type "DELETE ALL DATA" to proceed: ')
|
||||
if confirm_text != 'DELETE ALL DATA':
|
||||
self.stdout.write(self.style.WARNING('\nAborted. Data was NOT deleted.\n'))
|
||||
return
|
||||
|
||||
self.stdout.write('\nProceeding with deletion...\n')
|
||||
|
||||
deleted_counts = {}
|
||||
failed_deletions = []
|
||||
|
||||
with transaction.atomic():
|
||||
for name, model in models_to_clear:
|
||||
try:
|
||||
count = model.objects.count()
|
||||
if count > 0:
|
||||
model.objects.all().delete()
|
||||
deleted_counts[name] = count
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Deleted {count:6d} {name}')
|
||||
)
|
||||
else:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'· Skipped {count:6d} {name} (already empty)')
|
||||
)
|
||||
except Exception as e:
|
||||
failed_deletions.append((name, str(e)))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'✗ Failed to delete {name}: {str(e)}')
|
||||
)
|
||||
|
||||
# Summary
|
||||
total_deleted = sum(deleted_counts.values())
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS(f'\nUser Data Cleanup Complete!\n'))
|
||||
self.stdout.write(f' Total records deleted: {total_deleted:,}')
|
||||
self.stdout.write(f' Failed deletions: {len(failed_deletions)}')
|
||||
|
||||
if failed_deletions:
|
||||
self.stdout.write(self.style.WARNING('\nFailed deletions:'))
|
||||
for name, error in failed_deletions:
|
||||
self.stdout.write(f' - {name}: {error}')
|
||||
|
||||
self.stdout.write('\n' + '=' * 70 + '\n')
|
||||
122
backend/igny8_core/management/commands/export_system_config.py
Normal file
122
backend/igny8_core/management/commands/export_system_config.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Management command to export system configuration data to JSON files.
|
||||
This exports Plans, Credit Costs, AI Models, Industries, Sectors, Seed Keywords, etc.
|
||||
|
||||
Usage:
|
||||
python manage.py export_system_config --output-dir=backups/config
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core import serializers
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Export system configuration data to JSON files for V1.0 backup'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--output-dir',
|
||||
default='backups/config',
|
||||
help='Output directory for config files (relative to project root)'
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
output_dir = options['output_dir']
|
||||
|
||||
# Make output_dir absolute if it's relative
|
||||
if not os.path.isabs(output_dir):
|
||||
# Get project root (parent of manage.py)
|
||||
import sys
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
output_dir = os.path.join(project_root, '..', output_dir)
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'\nExporting system configuration to: {output_dir}\n'))
|
||||
|
||||
# Import models
|
||||
from igny8_core.modules.billing.models import Plan, CreditCostConfig
|
||||
from igny8_core.modules.system.models import AIModelConfig, GlobalIntegrationSettings
|
||||
from igny8_core.auth.models import Industry, Sector, SeedKeyword, AuthorProfile
|
||||
from igny8_core.ai.models import Prompt, PromptVariable
|
||||
|
||||
# Define what to export
|
||||
exports = {
|
||||
'plans': (Plan.objects.all(), 'Subscription Plans'),
|
||||
'credit_costs': (CreditCostConfig.objects.all(), 'Credit Cost Configurations'),
|
||||
'ai_models': (AIModelConfig.objects.all(), 'AI Model Configurations'),
|
||||
'global_integrations': (GlobalIntegrationSettings.objects.all(), 'Global Integration Settings'),
|
||||
'industries': (Industry.objects.all(), 'Industries'),
|
||||
'sectors': (Sector.objects.all(), 'Sectors'),
|
||||
'seed_keywords': (SeedKeyword.objects.all(), 'Seed Keywords'),
|
||||
'author_profiles': (AuthorProfile.objects.all(), 'Author Profiles'),
|
||||
'prompts': (Prompt.objects.all(), 'AI Prompts'),
|
||||
'prompt_variables': (PromptVariable.objects.all(), 'Prompt Variables'),
|
||||
}
|
||||
|
||||
successful_exports = []
|
||||
failed_exports = []
|
||||
|
||||
for name, (queryset, description) in exports.items():
|
||||
try:
|
||||
count = queryset.count()
|
||||
data = serializers.serialize('json', queryset, indent=2)
|
||||
filepath = os.path.join(output_dir, f'{name}.json')
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(data)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Exported {count:4d} {description:30s} → {name}.json')
|
||||
)
|
||||
successful_exports.append(name)
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'✗ Failed to export {description}: {str(e)}')
|
||||
)
|
||||
failed_exports.append((name, str(e)))
|
||||
|
||||
# Export metadata
|
||||
metadata = {
|
||||
'exported_at': datetime.now().isoformat(),
|
||||
'django_version': self.get_django_version(),
|
||||
'database': self.get_database_info(),
|
||||
'successful_exports': successful_exports,
|
||||
'failed_exports': failed_exports,
|
||||
'export_count': len(successful_exports),
|
||||
}
|
||||
|
||||
metadata_path = os.path.join(output_dir, 'export_metadata.json')
|
||||
with open(metadata_path, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'\n✓ Metadata saved to export_metadata.json'))
|
||||
|
||||
# Summary
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(self.style.SUCCESS(f'\nSystem Configuration Export Complete!\n'))
|
||||
self.stdout.write(f' Successful: {len(successful_exports)} exports')
|
||||
self.stdout.write(f' Failed: {len(failed_exports)} exports')
|
||||
self.stdout.write(f' Location: {output_dir}\n')
|
||||
|
||||
if failed_exports:
|
||||
self.stdout.write(self.style.WARNING('\nFailed exports:'))
|
||||
for name, error in failed_exports:
|
||||
self.stdout.write(f' - {name}: {error}')
|
||||
|
||||
self.stdout.write('=' * 70 + '\n')
|
||||
|
||||
def get_django_version(self):
|
||||
import django
|
||||
return django.get_version()
|
||||
|
||||
def get_database_info(self):
|
||||
from django.conf import settings
|
||||
db_config = settings.DATABASES.get('default', {})
|
||||
return {
|
||||
'engine': db_config.get('ENGINE', '').split('.')[-1],
|
||||
'name': db_config.get('NAME', ''),
|
||||
}
|
||||
Reference in New Issue
Block a user