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-27 04:15 +0000

1""" 

2Django management command to create a FormKit schema interactively. 

3 

4This command guides users through creating a FormKit schema with groups, 

5repeaters, and input fields. 

6""" 

7 

8import json 

9import uuid 

10 

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

12 

13from formkit_ninja import models 

14 

15 

16class Command(BaseCommand): 

17 """Create a FormKit schema interactively.""" 

18 

19 help = "Create a FormKit schema interactively with groups, repeaters, and fields" 

20 

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 ) 

35 

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

37 """Execute the command.""" 

38 label = options["label"] 

39 json_file = options.get("from_json") 

40 

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

44 

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) 

53 

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

61 

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

71 

72 # Create schema 

73 schema = models.FormKitSchema.objects.create(label=label) 

74 

75 # Process nodes recursively 

76 self._process_nodes(schema, schema_data, parent=None) 

77 

78 return schema 

79 

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) 

86 

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 ) 

93 

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 ) 

102 

103 # Add parent relationship if needed 

104 if parent: 

105 models.NodeChildren.objects.create( 

106 parent=parent, 

107 child=node, 

108 order=order, 

109 ) 

110 

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) 

114 

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

119 

120 # Create schema 

121 schema = models.FormKitSchema.objects.create(label=label) 

122 self.stdout.write(self.style.SUCCESS(f"✓ Created schema: {label}\n")) 

123 

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) 

128 

129 root_node = models.FormKitSchemaNode.objects.create( 

130 node={"$formkit": "group", "name": root_name}, 

131 label=root_label, 

132 ) 

133 

134 models.FormComponents.objects.create( 

135 schema=schema, 

136 node=root_node, 

137 label=root_label, 

138 order=0, 

139 ) 

140 

141 self.stdout.write(self.style.SUCCESS(f"✓ Created root group: {root_name}\n")) 

142 

143 # Add child nodes 

144 self._add_children_interactively(root_node) 

145 

146 return schema 

147 

148 def _add_children_interactively(self, parent_node: models.FormKitSchemaNode): 

149 """Add child nodes interactively.""" 

150 order = 0 

151 

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

155 

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 ) 

173 

174 if node_type == "done": 

175 break 

176 

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

180 

181 # Create node 

182 node = models.FormKitSchemaNode.objects.create( 

183 node={"$formkit": node_type, "name": name}, 

184 label=label, 

185 ) 

186 

187 # Add as child 

188 models.NodeChildren.objects.create( 

189 parent=parent_node, 

190 child=node, 

191 order=order, 

192 ) 

193 

194 order += 1 

195 

196 self.stdout.write(self.style.SUCCESS(f"✓ Added {node_type} field: {name}")) 

197 

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) 

203 

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

210 

211 value = input(prompt).strip() 

212 return value if value else default 

213 

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

220 

221 while True: 

222 value = input(f"\nEnter choice [1-{len(choices)}] or name: ").strip() 

223 

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] 

229 

230 # Check if it's a choice name 

231 if value in choices: 

232 return value 

233 

234 # Use default if empty 

235 if not value and default: 

236 return default 

237 

238 self.stdout.write(self.style.ERROR("Invalid choice. Try again.")) 

239 

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

244 

245 if not value: 

246 return default 

247 

248 return value in ["y", "yes", "true", "1"]