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
« 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.
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"""
10from pathlib import Path
12from django.core.management.base import BaseCommand, CommandError
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
21class Command(BaseCommand):
22 """Add fields to an existing schema and regenerate code."""
24 help = "Add fields to an existing FormKit schema and regenerate code"
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 )
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")
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.")
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")
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
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
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
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")
123 self.stdout.write(f"Adding field to parent: {parent_name} ({parent_formkit})")
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")
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}'")
135 # Create the new field
136 self.stdout.write(f"\nCreating new field: {field_name} ({field_type})")
138 new_node = models.FormKitSchemaNode.objects.create(
139 node={"$formkit": field_type, "name": field_name},
140 label=field_label,
141 )
143 # Get current max order for this parent
144 max_order = models.NodeChildren.objects.filter(parent=parent_node).count()
146 # Add as child
147 models.NodeChildren.objects.create(
148 parent=parent_node,
149 child=new_node,
150 order=max_order,
151 )
153 self.stdout.write(self.style.SUCCESS(f"✓ Added field: {field_name}"))
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"))
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})")
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')
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()
183 if not app_dir.exists():
184 raise CommandError(f"App directory does not exist: {app_dir}")
186 template_loader = DefaultTemplateLoader()
187 formatter = CodeFormatter()
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 )
200 # Convert schema to Pydantic format
201 values = list(schema.get_schema_values(recursive=True))
202 pydantic_schema = formkit_schema.FormKitSchema.parse_obj(values)
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