Coverage for formkit_ninja / management / commands / bootstrap_app.py: 0.00%

86 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-20 04:40 +0000

1""" 

2Django management command to bootstrap a complete Django app from a FormKit schema. 

3 

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""" 

10 

11from pathlib import Path 

12 

13from django.core.management import call_command 

14from django.core.management.base import BaseCommand, CommandError 

15 

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 

21 

22 

23class Command(BaseCommand): 

24 """Bootstrap a complete Django app from a FormKit schema.""" 

25 

26 help = "Bootstrap a complete Django app from a FormKit schema with models, admin, API, and signals" 

27 

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 ) 

53 

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) 

60 

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.") 

64 

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") 

70 

71 # Determine app directory 

72 app_dir = Path(app_dir_str).resolve() / app_name 

73 

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}") 

79 

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) 

84 

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}") 

94 

95 # Step 2: Generate code from schema 

96 self.stdout.write(f"\nGenerating code from schema: {schema_label}") 

97 

98 template_loader = DefaultTemplateLoader() 

99 formatter = CodeFormatter() 

100 

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 ) 

111 

112 # Convert schema to Pydantic format 

113 values = list(schema.get_schema_values(recursive=True)) 

114 pydantic_schema = formkit_schema.FormKitSchema.parse_obj(values) 

115 

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 

122 

123 # Step 3: Create signals file 

124 self.stdout.write("\nSignals file generated by CodeGenerator.") 

125 

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" 

129 

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 

137 

138 # Step 5: Create __init__.py with default_app_config 

139 self.stdout.write("\nUpdating __init__.py...") 

140 init_file = app_dir / "__init__.py" 

141 

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}")) 

149 

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}") 

157 

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") 

163 

164 def _generate_apps_file(self, app_name: str) -> str: 

165 """Generate the apps.py file content.""" 

166 config_name = app_name.capitalize() + "Config" 

167 

168 return f'''""" 

169Django app configuration for {app_name}. 

170""" 

171 

172from django.apps import AppConfig 

173 

174 

175class {config_name}(AppConfig): 

176 """Configuration for {app_name} app.""" 

177  

178 default_auto_field = 'django.db.models.BigAutoField' 

179 name = '{app_name}' 

180 

181 def ready(self): 

182 """Import signal handlers when Django starts.""" 

183 # Import signals to register handlers 

184 from . import signals # noqa: F401 

185'''