diff --git a/backend/igny8_core/management/commands/cleanup_user_data.py b/backend/igny8_core/management/commands/cleanup_user_data.py new file mode 100644 index 00000000..90010444 --- /dev/null +++ b/backend/igny8_core/management/commands/cleanup_user_data.py @@ -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') diff --git a/backend/igny8_core/management/commands/export_system_config.py b/backend/igny8_core/management/commands/export_system_config.py new file mode 100644 index 00000000..80eacadf --- /dev/null +++ b/backend/igny8_core/management/commands/export_system_config.py @@ -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', ''), + }