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

99 statements  

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

1""" 

2Django management command to add fields to an existing FormKit schema and regenerate code. 

3 

4This command allows users to: 

51. Add new fields to an existing schema 

62. Automatically regenerate Django models, schemas, admin, and API code 

73. Show a diff of what changed 

8""" 

9 

10from pathlib import Path 

11 

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

13 

14from formkit_ninja import formkit_schema, models 

15from formkit_ninja.parser.formatter import CodeFormatter 

16from formkit_ninja.parser.generator import CodeGenerator 

17from formkit_ninja.parser.generator_config import GeneratorConfig 

18from formkit_ninja.parser.template_loader import DefaultTemplateLoader 

19 

20 

21class Command(BaseCommand): 

22 """Add fields to an existing schema and regenerate code.""" 

23 

24 help = "Add fields to an existing FormKit schema and regenerate code" 

25 

26 def add_arguments(self, parser): 

27 """Add command-line arguments.""" 

28 parser.add_argument( 

29 "--schema-label", 

30 type=str, 

31 required=True, 

32 help="Label of the schema to modify (required)", 

33 ) 

34 parser.add_argument( 

35 "--parent-node", 

36 type=str, 

37 default=None, 

38 help="Name of the parent node to add fields to (default: root group)", 

39 ) 

40 parser.add_argument( 

41 "--field-type", 

42 type=str, 

43 required=True, 

44 help="Type of field to add (text, number, email, group, repeater, etc.)", 

45 ) 

46 parser.add_argument( 

47 "--field-name", 

48 type=str, 

49 required=True, 

50 help="Name of the field (required)", 

51 ) 

52 parser.add_argument( 

53 "--field-label", 

54 type=str, 

55 default=None, 

56 help="Label of the field (default: derived from name)", 

57 ) 

58 parser.add_argument( 

59 "--app-name", 

60 type=str, 

61 default=None, 

62 help="Django app name to regenerate code for (optional)", 

63 ) 

64 parser.add_argument( 

65 "--app-dir", 

66 type=str, 

67 default=None, 

68 help="Directory of the Django app (required if --app-name is provided)", 

69 ) 

70 

71 def handle(self, *args, **options): 

72 """Execute the command.""" 

73 schema_label = options["schema_label"] 

74 parent_node_name = options.get("parent_node") 

75 field_type = options["field_type"] 

76 field_name = options["field_name"] 

77 field_label = options.get("field_label") or field_name.replace("_", " ").title() 

78 app_name = options.get("app_name") 

79 app_dir_str = options.get("app_dir") 

80 

81 # Validate field name 

82 if not field_name.isidentifier(): 

83 raise CommandError(f"Invalid field name: {field_name}. Must be a valid Python identifier.") 

84 

85 # Get the schema 

86 try: 

87 schema = models.FormKitSchema.objects.get(label=schema_label) 

88 except models.FormKitSchema.DoesNotExist: 

89 raise CommandError(f"Schema with label '{schema_label}' not found") 

90 

91 # Find parent node 

92 if parent_node_name: 

93 # Find the node by searching through schema nodes 

94 parent_node = None 

95 for component in models.FormComponents.objects.filter(schema=schema): 

96 if component.node and component.node.node and component.node.node.get("name") == parent_node_name: 

97 parent_node = component.node 

98 break 

99 

100 if not parent_node: 

101 # Also check children of components 

102 for node in models.FormKitSchemaNode.objects.all(): 

103 if node.node and node.node.get("name") == parent_node_name: 

104 parent_node = node 

105 break 

106 

107 if not parent_node: 

108 raise CommandError(f"Parent node '{parent_node_name}' not found in schema '{schema_label}'") 

109 else: 

110 # Use root group (first component) 

111 try: 

112 parent_component = models.FormComponents.objects.filter(schema=schema).order_by("order").first() 

113 if not parent_component or not parent_component.node: 

114 raise CommandError(f"No root node found in schema '{schema_label}'") 

115 parent_node = parent_component.node 

116 except Exception as e: 

117 raise CommandError(f"Failed to find root node: {e}") from e 

118 

119 parent_node_data = parent_node.node or {} 

120 parent_name = parent_node_data.get("name", "unknown") 

121 parent_formkit = parent_node_data.get("$formkit", "unknown") 

122 

123 self.stdout.write(f"Adding field to parent: {parent_name} ({parent_formkit})") 

124 

125 # Check if parent can have children 

126 if parent_formkit not in ["group", "repeater"]: 

127 raise CommandError(f"Parent node '{parent_name}' is not a group or repeater") 

128 

129 # Check if field already exists 

130 for child_rel in models.NodeChildren.objects.filter(parent=parent_node): 

131 child_data = child_rel.child.node or {} 

132 if child_data.get("name") == field_name: 

133 raise CommandError(f"Field '{field_name}' already exists in '{parent_name}'") 

134 

135 # Create the new field 

136 self.stdout.write(f"\nCreating new field: {field_name} ({field_type})") 

137 

138 new_node = models.FormKitSchemaNode.objects.create( 

139 node={"$formkit": field_type, "name": field_name}, 

140 label=field_label, 

141 ) 

142 

143 # Get current max order for this parent 

144 max_order = models.NodeChildren.objects.filter(parent=parent_node).count() 

145 

146 # Add as child 

147 models.NodeChildren.objects.create( 

148 parent=parent_node, 

149 child=new_node, 

150 order=max_order, 

151 ) 

152 

153 self.stdout.write(self.style.SUCCESS(f"✓ Added field: {field_name}")) 

154 

155 # Regenerate code if app info provided 

156 if app_name and app_dir_str: 

157 self.stdout.write(f"\nRegenerating code for app: {app_name}") 

158 self._regenerate_code(schema, app_name, app_dir_str) 

159 elif app_name or app_dir_str: 

160 self.stdout.write(self.style.WARNING("\nWarning: Both --app-name and --app-dir are required to regenerate code")) 

161 

162 # Summary 

163 self.stdout.write("\n" + "=" * 70) 

164 self.stdout.write(self.style.SUCCESS("✓ Field added successfully!")) 

165 self.stdout.write("=" * 70) 

166 self.stdout.write(f"\nSchema: {schema_label}") 

167 self.stdout.write(f"Parent: {parent_name}") 

168 self.stdout.write(f"New field: {field_name} ({field_type})") 

169 

170 if app_name and app_dir_str: 

171 self.stdout.write("\n" + self.style.WARNING("Next steps:")) 

172 self.stdout.write("1. Review the generated code changes") 

173 self.stdout.write("2. Run migrations: ./manage.py makemigrations && ./manage.py migrate") 

174 self.stdout.write("3. Test the updated API and admin interface\n") 

175 else: 

176 self.stdout.write("\n" + self.style.WARNING("To regenerate code, run:")) 

177 self.stdout.write(f' ./manage.py add_schema_field --schema-label "{schema_label}" --field-type {field_type} --field-name {field_name} --app-name YOUR_APP --app-dir ./YOUR_APP\n') 

178 

179 def _regenerate_code(self, schema: models.FormKitSchema, app_name: str, app_dir_str: str): 

180 """Regenerate code for the app.""" 

181 app_dir = Path(app_dir_str).resolve() 

182 

183 if not app_dir.exists(): 

184 raise CommandError(f"App directory does not exist: {app_dir}") 

185 

186 template_loader = DefaultTemplateLoader() 

187 formatter = CodeFormatter() 

188 

189 config = GeneratorConfig( 

190 app_name=app_name, 

191 output_dir=app_dir, 

192 schema_name=schema.label, 

193 ) 

194 generator = CodeGenerator( 

195 config=config, 

196 template_loader=template_loader, 

197 formatter=formatter, 

198 ) 

199 

200 # Convert schema to Pydantic format 

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

202 pydantic_schema = formkit_schema.FormKitSchema.parse_obj(values) 

203 

204 # Generate code 

205 try: 

206 generator.generate(pydantic_schema) 

207 self.stdout.write(self.style.SUCCESS("✓ Regenerated models, schemas, admin, and API code")) 

208 except Exception as e: 

209 raise CommandError(f"Failed to regenerate code: {e}") from e