Coverage for formkit_ninja / admin_code_generation.py: 0.00%
115 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-03 09:21 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-03 09:21 +0000
1"""
2Admin interface for CodeGenerationConfig model.
4Provides a user-friendly interface for managing database-driven code generation rules,
5including custom JSON widgets for better editing experience.
6"""
8from django import forms
9from django.contrib import admin
11from formkit_ninja.code_generation_config import CodeGenerationConfig
14class PrettyJSONWidget(forms.Textarea):
15 """
16 Custom widget for JSONField that formats JSON nicely and provides better editing.
17 """
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:
27 default_attrs.update(attrs)
28 super().__init__(attrs=default_attrs)
30 def format_value(self, value):
31 """Format the JSON value nicely for display."""
32 if value is None or value == "":
33 return ""
35 import json
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
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
51class CodeGenerationConfigAdminForm(forms.ModelForm):
52 """
53 Custom admin form for CodeGenerationConfig.
55 Provides better help text and validation for JSON fields.
56 """
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 }
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")
103 # At least formkit_type should be set
104 if not formkit_type:
105 raise forms.ValidationError("formkit_type is required")
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)")
119 return cleaned_data
122@admin.register(CodeGenerationConfig)
123class CodeGenerationConfigAdmin(admin.ModelAdmin):
124 """
125 Admin interface for CodeGenerationConfig.
127 Provides filtering, searching, and organized fieldsets for easy management.
128 """
130 form = CodeGenerationConfigAdminForm
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 )
143 list_filter = (
144 "is_active",
145 "formkit_type",
146 ("node_name", admin.EmptyFieldListFilter),
147 ("options_pattern", admin.EmptyFieldListFilter),
148 "created",
149 )
151 search_fields = (
152 "formkit_type",
153 "node_name",
154 "options_pattern",
155 "pydantic_type",
156 "django_type",
157 )
159 readonly_fields = ("created", "updated", "django_code_preview", "pydantic_code_preview")
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 )
207 ordering = ("-priority", "formkit_type", "node_name")
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
214 from formkit_ninja.parser.type_convert import NodePath
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 ""
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()
228 def to_django_args(self):
229 if obj.django_args:
230 return obj.get_django_args_str()
231 return super().to_django_args()
233 def get_validators(self):
234 return obj.validators or super().get_validators()
236 def get_extra_imports(self):
237 return obj.extra_imports or super().get_extra_imports()
239 node = MockNode(obj.formkit_type, obj.node_name)
240 path = PreviewNodePath(node)
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))
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
258 from formkit_ninja.parser.type_convert import NodePath
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 ""
266 class PreviewNodePath(NodePath):
267 def to_pydantic_type(self):
268 return obj.pydantic_type or super().to_pydantic_type()
270 def to_django_type(self):
271 return obj.django_type or super().to_django_type()
273 node = MockNode(obj.formkit_type, obj.node_name)
274 path = PreviewNodePath(node)
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))
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)
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)
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)
308 def save_model(self, request, obj, form, change):
309 """Add helpful feedback when saving."""
310 super().save_model(request, obj, form, change)
312 from django.contrib import messages
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}")