Coverage for formkit_ninja / management / commands / create_schema.py: 0.00%
108 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-20 04:40 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-20 04:40 +0000
1"""
2Django management command to create a FormKit schema interactively.
4This command guides users through creating a FormKit schema with groups,
5repeaters, and input fields.
6"""
8import json
9import uuid
11from django.core.management.base import BaseCommand, CommandError
13from formkit_ninja import models
16class Command(BaseCommand):
17 """Create a FormKit schema interactively."""
19 help = "Create a FormKit schema interactively with groups, repeaters, and fields"
21 def add_arguments(self, parser):
22 """Add command-line arguments."""
23 parser.add_argument(
24 "--label",
25 type=str,
26 required=True,
27 help="Label for the schema (required)",
28 )
29 parser.add_argument(
30 "--from-json",
31 type=str,
32 default=None,
33 help="Path to JSON file containing schema definition (optional)",
34 )
36 def handle(self, *args, **options):
37 """Execute the command."""
38 label = options["label"]
39 json_file = options.get("from_json")
41 # Check if schema already exists
42 if models.FormKitSchema.objects.filter(label=label).exists():
43 raise CommandError(f"Schema with label '{label}' already exists")
45 if json_file:
46 # Load from JSON file
47 self.stdout.write(f"Loading schema from: {json_file}")
48 schema = self._create_from_json(label, json_file)
49 else:
50 # Interactive creation
51 self.stdout.write(self.style.SUCCESS(f"Creating schema: {label}"))
52 schema = self._create_interactively(label)
54 self.stdout.write("\n" + "=" * 70)
55 self.stdout.write(self.style.SUCCESS("✓ Schema created successfully!"))
56 self.stdout.write("=" * 70)
57 self.stdout.write(f"\nSchema label: {schema.label}")
58 self.stdout.write(f"Schema ID: {schema.id}")
59 self.stdout.write("\nYou can now use this schema with:")
60 self.stdout.write(f' ./manage.py bootstrap_app --schema-label "{label}" --app-name your_app_name\n')
62 def _create_from_json(self, label: str, json_file: str) -> models.FormKitSchema:
63 """Create schema from JSON file."""
64 try:
65 with open(json_file, "r") as f:
66 schema_data = json.load(f)
67 except FileNotFoundError:
68 raise CommandError(f"File not found: {json_file}")
69 except json.JSONDecodeError as e:
70 raise CommandError(f"Invalid JSON in file: {e}")
72 # Create schema
73 schema = models.FormKitSchema.objects.create(label=label)
75 # Process nodes recursively
76 self._process_nodes(schema, schema_data, parent=None)
78 return schema
80 def _process_nodes(self, schema: models.FormKitSchema, nodes: list, parent=None):
81 """Process nodes recursively."""
82 for order, node_data in enumerate(nodes):
83 formkit_type = node_data.get("$formkit", "text")
84 name = node_data.get("name", f"field_{uuid.uuid4().hex[:8]}")
85 label = node_data.get("label", name)
87 # Create node
88 node = models.FormKitSchemaNode.objects.create(
89 node={"$formkit": formkit_type, "name": name},
90 label=label,
91 additional_props=node_data,
92 )
94 # Add to schema ONLY if this is a root node (no parent)
95 if parent is None:
96 models.FormComponents.objects.create(
97 schema=schema,
98 node=node,
99 label=label,
100 order=order,
101 )
103 # Add parent relationship if needed
104 if parent:
105 models.NodeChildren.objects.create(
106 parent=parent,
107 child=node,
108 order=order,
109 )
111 # Process children if this is a group or repeater
112 if formkit_type in ["group", "repeater"] and "children" in node_data:
113 self._process_nodes(schema, node_data["children"], parent=node)
115 def _create_interactively(self, label: str) -> models.FormKitSchema:
116 """Create schema interactively."""
117 self.stdout.write("\nThis wizard will guide you through creating a FormKit schema.")
118 self.stdout.write("You can create groups, repeaters, and input fields.\n")
120 # Create schema
121 schema = models.FormKitSchema.objects.create(label=label)
122 self.stdout.write(self.style.SUCCESS(f"✓ Created schema: {label}\n"))
124 # Create root group
125 self.stdout.write("Creating root group node...")
126 root_name = self._prompt("Root group name", default=label.lower().replace(" ", "_"))
127 root_label = self._prompt("Root group label", default=label)
129 root_node = models.FormKitSchemaNode.objects.create(
130 node={"$formkit": "group", "name": root_name},
131 label=root_label,
132 )
134 models.FormComponents.objects.create(
135 schema=schema,
136 node=root_node,
137 label=root_label,
138 order=0,
139 )
141 self.stdout.write(self.style.SUCCESS(f"✓ Created root group: {root_name}\n"))
143 # Add child nodes
144 self._add_children_interactively(root_node)
146 return schema
148 def _add_children_interactively(self, parent_node: models.FormKitSchemaNode):
149 """Add child nodes interactively."""
150 order = 0
152 while True:
153 parent_name = parent_node.node.get("name", "node") if parent_node.node else "node"
154 self.stdout.write(f"\nAdding child to: {parent_name}")
156 # Ask what type of node to add
157 node_type = self._prompt_choice(
158 "Node type",
159 choices=[
160 "text",
161 "number",
162 "email",
163 "textarea",
164 "select",
165 "checkbox",
166 "date",
167 "group",
168 "repeater",
169 "done",
170 ],
171 default="done",
172 )
174 if node_type == "done":
175 break
177 # Get node details
178 name = self._prompt("Field name (e.g., 'email', 'age')") or f"field_{uuid.uuid4().hex[:8]}"
179 label = self._prompt("Field label (human-readable)", default=name.replace("_", " ").title())
181 # Create node
182 node = models.FormKitSchemaNode.objects.create(
183 node={"$formkit": node_type, "name": name},
184 label=label,
185 )
187 # Add as child
188 models.NodeChildren.objects.create(
189 parent=parent_node,
190 child=node,
191 order=order,
192 )
194 order += 1
196 self.stdout.write(self.style.SUCCESS(f"✓ Added {node_type} field: {name}"))
198 # If group or repeater, recursively add children
199 if node_type in ["group", "repeater"]:
200 add_children = self._prompt_yes_no(f"Add children to {name}?", default=True)
201 if add_children:
202 self._add_children_interactively(node)
204 def _prompt(self, message: str, default: str | None = None) -> str | None:
205 """Prompt user for input."""
206 if default:
207 prompt = f"{message} [{default}]: "
208 else:
209 prompt = f"{message}: "
211 value = input(prompt).strip()
212 return value if value else default
214 def _prompt_choice(self, message: str, choices: list, default: str | None = None) -> str:
215 """Prompt user to choose from a list."""
216 self.stdout.write(f"\n{message}:")
217 for i, choice in enumerate(choices, 1):
218 marker = " (default)" if choice == default else ""
219 self.stdout.write(f" {i}. {choice}{marker}")
221 while True:
222 value = input(f"\nEnter choice [1-{len(choices)}] or name: ").strip()
224 # Check if it's a number
225 if value.isdigit():
226 idx = int(value) - 1
227 if 0 <= idx < len(choices):
228 return choices[idx]
230 # Check if it's a choice name
231 if value in choices:
232 return value
234 # Use default if empty
235 if not value and default:
236 return default
238 self.stdout.write(self.style.ERROR("Invalid choice. Try again."))
240 def _prompt_yes_no(self, message: str, default: bool = True) -> bool:
241 """Prompt user for yes/no."""
242 default_str = "Y/n" if default else "y/N"
243 value = input(f"{message} [{default_str}]: ").strip().lower()
245 if not value:
246 return default
248 return value in ["y", "yes", "true", "1"]