Coverage for formkit_ninja / admin_code_generation.py: 28.57%

115 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-06 04:12 +0000

1""" 

2Admin interface for CodeGenerationConfig model. 

3 

4Provides a user-friendly interface for managing database-driven code generation rules, 

5including custom JSON widgets for better editing experience. 

6""" 

7 

8from django import forms 

9from django.contrib import admin 

10 

11from formkit_ninja.code_generation_config import CodeGenerationConfig 

12 

13 

14class PrettyJSONWidget(forms.Textarea): 

15 """ 

16 Custom widget for JSONField that formats JSON nicely and provides better editing. 

17 """ 

18 

19 def __init__(self, attrs=None): 

20 default_attrs = { 

21 "rows": 10, 

22 "cols": 80, 

23 "style": "font-family: monospace; font-size: 12px;", 

24 "placeholder": '{"key": "value"}', 

25 } 

26 if attrs: 26 ↛ 28line 26 didn't jump to line 28 because the condition on line 26 was always true

27 default_attrs.update(attrs) 

28 super().__init__(attrs=default_attrs) 

29 

30 def format_value(self, value): 

31 """Format the JSON value nicely for display.""" 

32 if value is None or value == "": 

33 return "" 

34 

35 import json 

36 

37 # If it's already a string, try to parse and re-format it 

38 if isinstance(value, str): 

39 try: 

40 value = json.loads(value) 

41 except (json.JSONDecodeError, TypeError): 

42 return value 

43 

44 # Format with indentation 

45 try: 

46 return json.dumps(value, indent=2, ensure_ascii=False, sort_keys=True) 

47 except (TypeError, ValueError): 

48 return value 

49 

50 

51class CodeGenerationConfigAdminForm(forms.ModelForm): 

52 """ 

53 Custom admin form for CodeGenerationConfig. 

54 

55 Provides better help text and validation for JSON fields. 

56 """ 

57 

58 class Meta: 

59 model = CodeGenerationConfig 

60 fields = "__all__" 

61 widgets = { 

62 "django_args": PrettyJSONWidget( 

63 attrs={ 

64 "rows": 8, 

65 "placeholder": '{\n "null": true,\n "blank": true,\n "max_length": 255\n}', 

66 } 

67 ), 

68 "extra_imports": forms.Textarea( 

69 attrs={ 

70 "rows": 4, 

71 "cols": 80, 

72 "placeholder": '["from decimal import Decimal", "from datetime import datetime"]', 

73 } 

74 ), 

75 "validators": forms.Textarea( 

76 attrs={ 

77 "rows": 4, 

78 "cols": 80, 

79 "placeholder": '["MinValueValidator(0)", "MaxValueValidator(100)"]', 

80 } 

81 ), 

82 } 

83 help_texts = { 

84 "formkit_type": "The FormKit `type` property to match (e.g., 'text', 'select', 'datepicker')", 

85 "node_name": "Field `name` to match (highest priority, leave blank for type-level)", 

86 "options_pattern": "Pattern to match in the `options` field (e.g., '$ida(')", 

87 "pydantic_type": "Override the Pydantic type (e.g., 'int', 'str', 'Decimal', 'date')", 

88 "django_type": "Override the Django field type (e.g., 'ForeignKey', 'DateField', 'CharField')", 

89 "django_args": 'Field arguments as JSON dict (e.g., {"null": true, "to": "app.Model"})', 

90 "extra_imports": 'Python imports as JSON array (e.g., ["from decimal import Decimal"])', 

91 "validators": 'Django validators as JSON array (e.g., ["MinValueValidator(0)"])', 

92 "priority": "Higher numbers = higher priority (use for ordering when multiple configs match)", 

93 "is_active": "Inactive configs are ignored during code generation", 

94 } 

95 

96 def clean(self): 

97 """Validate the configuration.""" 

98 cleaned_data = super().clean() 

99 formkit_type = cleaned_data.get("formkit_type") 

100 # node_name = cleaned_data.get("node_name") 

101 # options_pattern = cleaned_data.get("options_pattern") 

102 

103 # At least formkit_type should be set 

104 if not formkit_type: 

105 raise forms.ValidationError("formkit_type is required") 

106 

107 # Warn if no override is specified 

108 if not any( 

109 [ 

110 cleaned_data.get("pydantic_type"), 

111 cleaned_data.get("django_type"), 

112 cleaned_data.get("django_args"), 

113 cleaned_data.get("extra_imports"), 

114 cleaned_data.get("validators"), 

115 ] 

116 ): 

117 raise forms.ValidationError("At least one override field should be specified (pydantic_type, django_type, django_args, extra_imports, or validators)") 

118 

119 return cleaned_data 

120 

121 

122@admin.register(CodeGenerationConfig) 

123class CodeGenerationConfigAdmin(admin.ModelAdmin): 

124 """ 

125 Admin interface for CodeGenerationConfig. 

126 

127 Provides filtering, searching, and organized fieldsets for easy management. 

128 """ 

129 

130 form = CodeGenerationConfigAdminForm 

131 

132 list_display = ( 

133 "summary", 

134 "formkit_type", 

135 "node_name", 

136 "priority", 

137 "is_active", 

138 "has_pydantic_override", 

139 "has_django_override", 

140 "created", 

141 ) 

142 

143 list_filter = ( 

144 "is_active", 

145 "formkit_type", 

146 ("node_name", admin.EmptyFieldListFilter), 

147 ("options_pattern", admin.EmptyFieldListFilter), 

148 "created", 

149 ) 

150 

151 search_fields = ( 

152 "formkit_type", 

153 "node_name", 

154 "options_pattern", 

155 "pydantic_type", 

156 "django_type", 

157 ) 

158 

159 readonly_fields = ("created", "updated", "django_code_preview", "pydantic_code_preview") 

160 

161 fieldsets = ( 

162 ( 

163 "Matching Criteria", 

164 { 

165 "fields": ("formkit_type", "node_name", "options_pattern", "priority", "is_active"), 

166 "description": "Define which FormKit nodes this config applies to. Higher priority wins.", 

167 }, 

168 ), 

169 ( 

170 "Type Overrides", 

171 { 

172 "fields": ("pydantic_type", "django_type"), 

173 "description": "Override the default type conversions for Pydantic schemas and Django models.", 

174 }, 

175 ), 

176 ( 

177 "Code Preview (Live)", 

178 { 

179 "fields": ("django_code_preview", "pydantic_code_preview"), 

180 "description": ("Preview of how a field might look using this configuration. Note: Uses 'example_field' as a placeholder name."), 

181 }, 

182 ), 

183 ( 

184 "Field Configuration", 

185 { 

186 "fields": ("django_args",), 

187 "description": "Additional Django field arguments (null, blank, max_length, to, on_delete, etc.)", 

188 }, 

189 ), 

190 ( 

191 "Advanced", 

192 { 

193 "fields": ("extra_imports", "validators"), 

194 "description": "Additional imports and validators to include in generated code.", 

195 "classes": ("collapse",), 

196 }, 

197 ), 

198 ( 

199 "Metadata", 

200 { 

201 "fields": ("created", "updated"), 

202 "classes": ("collapse",), 

203 }, 

204 ), 

205 ) 

206 

207 ordering = ("-priority", "formkit_type", "node_name") 

208 

209 @admin.display(description="Django Model Preview") 

210 def django_code_preview(self, obj): 

211 """Show what the Django model field code will look like.""" 

212 from django.utils.html import format_html 

213 

214 from formkit_ninja.parser.type_convert import NodePath 

215 

216 # Create a mock node that matches this config's criteria 

217 class MockNode: 

218 def __init__(self, formkit, name): 

219 self.formkit = formkit 

220 self.name = name or "example_field" 

221 self.options = obj.options_pattern or "" 

222 

223 # Use a custom NodePath that forces this config's values 

224 class PreviewNodePath(NodePath): 

225 def to_django_type(self): 

226 return obj.django_type or super().to_django_type() 

227 

228 def to_django_args(self): 

229 if obj.django_args: 

230 return obj.get_django_args_str() 

231 return super().to_django_args() 

232 

233 def get_validators(self): 

234 return obj.validators or super().get_validators() 

235 

236 def get_extra_imports(self): 

237 return obj.extra_imports or super().get_extra_imports() 

238 

239 node = MockNode(obj.formkit_type, obj.node_name) 

240 path = PreviewNodePath(node) 

241 

242 try: 

243 code = path.django_code 

244 style = "background: #f8f9fa; padding: 10px; border-radius: 4px; border: 1px solid #dee2e6;" 

245 return format_html( 

246 '<pre style="{}">{}</pre>', 

247 style, 

248 code, 

249 ) 

250 except Exception as e: 

251 return format_html('<div style="color: red;">Error generating preview: {}</div>', str(e)) 

252 

253 @admin.display(description="Pydantic Schema Preview") 

254 def pydantic_code_preview(self, obj): 

255 """Show what the Pydantic schema field code will look like.""" 

256 from django.utils.html import format_html 

257 

258 from formkit_ninja.parser.type_convert import NodePath 

259 

260 class MockNode: 

261 def __init__(self, formkit, name): 

262 self.formkit = formkit 

263 self.name = name or "example_field" 

264 self.options = obj.options_pattern or "" 

265 

266 class PreviewNodePath(NodePath): 

267 def to_pydantic_type(self): 

268 return obj.pydantic_type or super().to_pydantic_type() 

269 

270 def to_django_type(self): 

271 return obj.django_type or super().to_django_type() 

272 

273 node = MockNode(obj.formkit_type, obj.node_name) 

274 path = PreviewNodePath(node) 

275 

276 try: 

277 code = path.pydantic_code 

278 style = "background: #f8f9fa; padding: 10px; border-radius: 4px; border: 1px solid #dee2e6;" 

279 return format_html( 

280 '<pre style="{}">{}</pre>', 

281 style, 

282 code, 

283 ) 

284 except Exception as e: 

285 return format_html('<div style="color: red;">Error generating preview: {}</div>', str(e)) 

286 

287 # Custom display methods 

288 @admin.display(description="Configuration", ordering="formkit_type") 

289 def summary(self, obj): 

290 """Display a summary of the configuration.""" 

291 parts = [obj.formkit_type] 

292 if obj.node_name: 

293 parts.append(f"[{obj.node_name}]") 

294 if obj.options_pattern: 

295 parts.append(f"({obj.options_pattern})") 

296 return " ".join(parts) 

297 

298 @admin.display(boolean=True, description="Pydantic") 

299 def has_pydantic_override(self, obj): 

300 """Check if Pydantic type is overridden.""" 

301 return bool(obj.pydantic_type) 

302 

303 @admin.display(boolean=True, description="Django") 

304 def has_django_override(self, obj): 

305 """Check if Django type or args are overridden.""" 

306 return bool(obj.django_type or obj.django_args) 

307 

308 def save_model(self, request, obj, form, change): 

309 """Add helpful feedback when saving.""" 

310 super().save_model(request, obj, form, change) 

311 

312 from django.contrib import messages 

313 

314 if not change: 

315 messages.success( 

316 request, 

317 f"Created code generation config for {obj.formkit_type}" + (f" field '{obj.node_name}'" if obj.node_name else ""), 

318 ) 

319 else: 

320 messages.info(request, f"Updated config: {obj}")