Coverage for formkit_ninja / management / commands / bootstrap_app.py: 0.00%
86 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-06 04:12 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-06 04:12 +0000
1"""
2Django management command to bootstrap a complete Django app from a FormKit schema.
4This command:
51. Creates a new Django app (if it doesn't exist)
62. Generates Django models, Pydantic schemas, admin classes, and API endpoints
73. Creates and attaches a signals file for handling form submissions
84. Updates settings.py to include the new app
9"""
11from pathlib import Path
13from django.core.management import call_command
14from django.core.management.base import BaseCommand, CommandError
16from formkit_ninja import formkit_schema, models
17from formkit_ninja.parser.formatter import CodeFormatter
18from formkit_ninja.parser.generator import CodeGenerator
19from formkit_ninja.parser.generator_config import GeneratorConfig
20from formkit_ninja.parser.template_loader import DefaultTemplateLoader
23class Command(BaseCommand):
24 """Bootstrap a complete Django app from a FormKit schema."""
26 help = "Bootstrap a complete Django app from a FormKit schema with models, admin, API, and signals"
28 def add_arguments(self, parser):
29 """Add command-line arguments."""
30 parser.add_argument(
31 "--schema-label",
32 type=str,
33 required=True,
34 help="Label of the FormKit schema to use (required)",
35 )
36 parser.add_argument(
37 "--app-name",
38 type=str,
39 required=True,
40 help="Name of the Django app to create (required)",
41 )
42 parser.add_argument(
43 "--app-dir",
44 type=str,
45 default=None,
46 help="Directory where the app will be created (default: current directory)",
47 )
48 parser.add_argument(
49 "--skip-startapp",
50 action="store_true",
51 help="Skip creating the Django app (use if app already exists)",
52 )
54 def handle(self, *args, **options):
55 """Execute the command."""
56 schema_label = options["schema_label"]
57 app_name = options["app_name"]
58 app_dir_str = options.get("app_dir") or "."
59 skip_startapp = options.get("skip_startapp", False)
61 # Validate app name
62 if not app_name.isidentifier():
63 raise CommandError(f"Invalid app name: {app_name}. Must be a valid Python identifier.")
65 # Get the schema
66 try:
67 schema = models.FormKitSchema.objects.get(label=schema_label)
68 except models.FormKitSchema.DoesNotExist:
69 raise CommandError(f"Schema with label '{schema_label}' not found")
71 # Determine app directory
72 app_dir = Path(app_dir_str).resolve() / app_name
74 # Step 1: Create Django app if needed
75 if not skip_startapp:
76 if app_dir.exists():
77 self.stdout.write(self.style.WARNING(f"App directory already exists: {app_dir}. Use --skip-startapp to continue."))
78 raise CommandError(f"App directory already exists: {app_dir}")
80 self.stdout.write(f"Creating Django app: {app_name}")
81 try:
82 # Ensure app directory exists (Django's startapp requires this)
83 app_dir.mkdir(parents=True, exist_ok=False)
85 # Create the app in the specified directory
86 call_command("startapp", app_name, str(app_dir))
87 self.stdout.write(self.style.SUCCESS(f"✓ Created Django app: {app_name}"))
88 except Exception as e:
89 raise CommandError(f"Failed to create Django app: {e}") from e
90 else:
91 if not app_dir.exists():
92 raise CommandError(f"App directory does not exist: {app_dir}. Remove --skip-startapp to create it.")
93 self.stdout.write(f"Using existing app directory: {app_dir}")
95 # Step 2: Generate code from schema
96 self.stdout.write(f"\nGenerating code from schema: {schema_label}")
98 template_loader = DefaultTemplateLoader()
99 formatter = CodeFormatter()
101 config = GeneratorConfig(
102 app_name=app_name,
103 output_dir=app_dir,
104 schema_name=schema_label,
105 )
106 generator = CodeGenerator(
107 config=config,
108 template_loader=template_loader,
109 formatter=formatter,
110 )
112 # Convert schema to Pydantic format
113 values = list(schema.get_schema_values(recursive=True))
114 pydantic_schema = formkit_schema.FormKitSchema.parse_obj(values)
116 # Generate code
117 try:
118 generator.generate(pydantic_schema)
119 self.stdout.write(self.style.SUCCESS("✓ Generated models, schemas, admin, and API code"))
120 except Exception as e:
121 raise CommandError(f"Failed to generate code: {e}") from e
123 # Step 3: Create signals file
124 self.stdout.write("\nSignals file generated by CodeGenerator.")
126 # Step 4: Update apps.py to connect signals
127 self.stdout.write("\nUpdating apps.py to connect signals...")
128 apps_file = app_dir / "apps.py"
130 try:
131 apps_content = self._generate_apps_file(app_name)
132 with open(apps_file, "w") as f:
133 f.write(apps_content)
134 self.stdout.write(self.style.SUCCESS(f"✓ Updated apps.py: {apps_file}"))
135 except Exception as e:
136 raise CommandError(f"Failed to update apps.py: {e}") from e
138 # Step 5: Create __init__.py with default_app_config
139 self.stdout.write("\nUpdating __init__.py...")
140 init_file = app_dir / "__init__.py"
142 try:
143 init_content = f'default_app_config = "{app_name}.apps.{app_name.capitalize()}Config"\n'
144 with open(init_file, "w") as f:
145 f.write(init_content)
146 self.stdout.write(self.style.SUCCESS(f"✓ Updated __init__.py: {init_file}"))
147 except Exception as e:
148 self.stdout.write(self.style.WARNING(f"Failed to update __init__.py: {e}"))
150 # Summary
151 self.stdout.write("\n" + "=" * 70)
152 self.stdout.write(self.style.SUCCESS("✓ App bootstrap complete!"))
153 self.stdout.write("=" * 70)
154 self.stdout.write(f"\nApp name: {app_name}")
155 self.stdout.write(f"App directory: {app_dir}")
156 self.stdout.write(f"Schema: {schema_label}")
158 self.stdout.write("\n" + self.style.WARNING("Next steps:"))
159 self.stdout.write(f"1. Add '{app_name}' to INSTALLED_APPS in settings.py")
160 self.stdout.write("2. Run migrations: ./manage.py makemigrations && ./manage.py migrate")
161 self.stdout.write("3. Test the API endpoints and admin interface")
162 self.stdout.write("4. Submit form data to see signals in action\n")
164 def _generate_apps_file(self, app_name: str) -> str:
165 """Generate the apps.py file content."""
166 config_name = app_name.capitalize() + "Config"
168 return f'''"""
169Django app configuration for {app_name}.
170"""
172from django.apps import AppConfig
175class {config_name}(AppConfig):
176 """Configuration for {app_name} app."""
178 default_auto_field = 'django.db.models.BigAutoField'
179 name = '{app_name}'
181 def ready(self):
182 """Import signal handlers when Django starts."""
183 # Import signals to register handlers
184 from . import signals # noqa: F401
185'''