Coverage for formkit_ninja / code_generation_config.py: 27.50%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-20 04:40 +0000

1""" 

2Code generation configuration model. 

3 

4This model stores database-driven configuration for code generation, 

5allowing users to override type mappings and field arguments via Django admin 

6instead of creating Python subclasses. 

7""" 

8 

9from django.db import models 

10 

11 

12class CodeGenerationConfig(models.Model): 

13 """ 

14 Database-stored configuration for code generation. 

15 

16 Priority cascade: 

17 1. Node-specific match (by node_name) 

18 2. Options pattern match (by options_pattern) 

19 3. FormKit type match (by formkit_type) 

20 4. Django settings 

21 5. Default converters 

22 

23 Example configurations: 

24 - FormKit type level: formkit_type="datepicker", django_type="DateField" 

25 - Node-specific: node_name="district", django_type="ForeignKey" 

26 - Options pattern: options_pattern="$ida(", pydantic_type="int" 

27 """ 

28 

29 # Matching criteria 

30 formkit_type = models.CharField( 

31 max_length=100, 

32 db_index=True, 

33 help_text='FormKit type (e.g., "text", "datepicker", "repeater")', 

34 ) 

35 node_name = models.CharField( 

36 max_length=256, 

37 null=True, 

38 blank=True, 

39 db_index=True, 

40 help_text="Optional: match specific node name (higher priority than formkit_type)", 

41 ) 

42 options_pattern = models.CharField( 

43 max_length=256, 

44 null=True, 

45 blank=True, 

46 help_text='Optional: match if options starts with this pattern (e.g., "$ida(")', 

47 ) 

48 

49 # Type overrides 

50 pydantic_type = models.CharField( 

51 max_length=100, 

52 null=True, 

53 blank=True, 

54 help_text='Override Pydantic type (e.g., "int", "str", "Decimal", "date")', 

55 ) 

56 django_type = models.CharField( 

57 max_length=100, 

58 null=True, 

59 blank=True, 

60 help_text='Override Django field type (e.g., "ForeignKey", "IntegerField", "DateField")', 

61 ) 

62 django_args = models.JSONField( 

63 default=dict, 

64 blank=True, 

65 help_text='Django field arguments as JSON dict (e.g., {"null": true, "blank": true, "to": "app.Model"})', 

66 ) 

67 django_positional_args = models.JSONField( 

68 default=list, 

69 blank=True, 

70 help_text='Django field positional arguments as JSON list (e.g., ["auth.User"])', 

71 ) 

72 

73 # Extra configuration 

74 extra_imports = models.JSONField( 

75 default=list, 

76 blank=True, 

77 help_text='List of import statements to add to generated files (e.g., ["from decimal import Decimal"])', 

78 ) 

79 validators = models.JSONField( 

80 default=list, 

81 blank=True, 

82 help_text="List of validator strings (e.g., Pydantic field_validator decorators)", 

83 ) 

84 

85 # Priority and status 

86 priority = models.IntegerField( 

87 default=0, 

88 help_text="Matching priority (higher = checked first). Use for fine-grained control.", 

89 ) 

90 is_active = models.BooleanField( 

91 default=True, 

92 help_text="Set to False to disable this configuration without deleting it", 

93 ) 

94 

95 # Tracking 

96 created = models.DateTimeField(auto_now_add=True) 

97 updated = models.DateTimeField(auto_now=True) 

98 

99 class Meta: 

100 ordering = ["-priority", "formkit_type", "node_name"] 

101 constraints = [ 

102 models.UniqueConstraint( 

103 fields=["formkit_type", "node_name", "options_pattern"], 

104 name="unique_code_gen_config", 

105 ) 

106 ] 

107 verbose_name = "Code generation config" 

108 verbose_name_plural = "Code generation configs" 

109 

110 def __str__(self): 

111 parts = [f"{self.formkit_type}"] 

112 if self.node_name: 

113 parts.append(f"name={self.node_name}") 

114 if self.options_pattern: 

115 parts.append(f"opts={self.options_pattern}") 

116 return " | ".join(parts) 

117 

118 def get_django_args_str(self) -> str: 

119 """Convert django_args dict and positional args to string format for generated code.""" 

120 parts = [] 

121 

122 # Add positional arguments first 

123 if self.django_positional_args: 

124 for value in self.django_positional_args: 

125 if isinstance(value, bool): 

126 parts.append(str(value)) 

127 elif isinstance(value, (int, float)): 

128 parts.append(str(value)) 

129 elif isinstance(value, str): 

130 # Handle model references 

131 if value.startswith("models.") or "." in value and not value.startswith('"'): 

132 parts.append(value) 

133 else: 

134 parts.append(f'"{value}"') 

135 else: 

136 parts.append(str(value)) 

137 

138 # Add keyword arguments 

139 if self.django_args: 

140 for key, value in self.django_args.items(): 

141 if isinstance(value, bool): 

142 parts.append(f"{key}={str(value)}") 

143 elif isinstance(value, (int, float)): 

144 parts.append(f"{key}={value}") 

145 elif isinstance(value, str): 

146 # Handle model references (e.g., "app.Model" or already quoted "'app.Model'") 

147 if value.startswith("models.") or "." in value and not value.startswith('"'): 

148 # Model reference or function like models.CASCADE 

149 parts.append(f"{key}={value}") 

150 else: 

151 # String value 

152 parts.append(f'{key}="{value}"') 

153 else: 

154 # Other types, convert to string 

155 parts.append(f"{key}={value}") 

156 

157 return ", ".join(parts)