Coverage for family/admin_mixins.py: 23%

85 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-05 02:45 +0800

1""" 

2Admin mixins for Family Knowledge Management System 

3Enhanced admin functionality including inline creation 

4""" 

5 

6from django.contrib import admin 

7from django.urls import reverse 

8from django.utils.html import format_html 

9from django.http import JsonResponse 

10from django.shortcuts import render 

11from django.contrib.admin.views.main import ChangeList 

12 

13 

14class InlineCreateMixin: 

15 """ 

16 Mixin to add inline creation capability to admin forms 

17 Adds '+' buttons next to foreign key and many-to-many fields 

18 """ 

19 

20 # Define which fields should have inline creation buttons 

21 inline_create_fields = [] 

22 

23 def formfield_for_foreignkey(self, db_field, request, **kwargs): 

24 """ 

25 Override to add inline create button for foreign key fields 

26 """ 

27 formfield = super().formfield_for_foreignkey(db_field, request, **kwargs) 

28 

29 if db_field.name in self.inline_create_fields: 

30 formfield.widget = self.get_inline_create_widget( 

31 formfield.widget, 

32 db_field.related_model 

33 ) 

34 

35 return formfield 

36 

37 def formfield_for_manytomany(self, db_field, request, **kwargs): 

38 """ 

39 Override to add inline create button for many-to-many fields 

40 """ 

41 formfield = super().formfield_for_manytomany(db_field, request, **kwargs) 

42 

43 if db_field.name in self.inline_create_fields: 43 ↛ 44line 43 didn't jump to line 44 because the condition on line 43 was never true

44 formfield.widget = self.get_inline_create_widget( 

45 formfield.widget, 

46 db_field.related_model 

47 ) 

48 

49 return formfield 

50 

51 def get_inline_create_widget(self, original_widget, related_model): 

52 """ 

53 Create a widget wrapper that includes an inline create button 

54 """ 

55 app_label = related_model._meta.app_label 

56 model_name = related_model._meta.model_name 

57 

58 class InlineCreateWidgetWrapper: 

59 def __init__(self, widget): 

60 self.widget = widget 

61 self.related_model = related_model 

62 self.app_label = app_label 

63 self.model_name = model_name 

64 

65 def render(self, name, value, attrs=None, renderer=None): 

66 # Get the original widget HTML 

67 original_html = self.widget.render(name, value, attrs, renderer) 

68 

69 # Add the create button 

70 create_url = reverse(f'admin:{app_label}_{model_name}_add') 

71 # Check if this is a FilteredSelectMultiple widget (transfer widget) 

72 is_transfer_widget = 'FilteredSelectMultiple' in str(type(self.widget)) 

73 

74 if is_transfer_widget: 

75 # For transfer widgets, inject the button into the available objects area 

76 create_button = self._inject_button_into_transfer_widget( 

77 original_html, create_url, name, related_model._meta.verbose_name 

78 ) 

79 else: 

80 # For regular widgets, wrap normally 

81 create_button = format_html( 

82 ''' 

83 <div class="inline-create-wrapper"> 

84 {original} 

85 <a href="{url}" class="inline-create-btn"  

86 onclick="return showInlineCreatePopup(this, '{field_name}');" 

87 title="添加新{verbose_name}"> 

88 <span class="create-icon">+</span> 

89 <span class="create-text">新建</span> 

90 </a> 

91 </div> 

92 ''', 

93 original=original_html, 

94 url=create_url, 

95 field_name=name, 

96 verbose_name=related_model._meta.verbose_name 

97 ) 

98 

99 return create_button 

100 

101 def _inject_button_into_transfer_widget(self, original_html, create_url, field_name, verbose_name): 

102 """ 

103 Inject the create button into the available objects section of transfer widget 

104 """ 

105 # Look for the available objects section header or filter input 

106 import re 

107 

108 # Try to find the "Available" header or filter input in the transfer widget 

109 available_pattern = r'(<h2[^>]*>.*?可选.*?</h2>|<p[^>]*class="help"[^>]*>.*?可选.*?</p>|<label[^>]*>.*?Filter.*?</label>)' 

110 filter_pattern = r'(<input[^>]*id="id_' + re.escape(field_name) + r'_input"[^>]*>)' 

111 

112 create_button_html = format_html( 

113 ''' 

114 <div class="transfer-widget-create-btn"> 

115 <a href="{url}" class="inline-create-btn compact"  

116 onclick="return showInlineCreatePopup(this, '{field_name}');" 

117 title="添加新{verbose_name}"> 

118 <span class="create-icon">+</span> 

119 <span class="create-text">新建{verbose_name}</span> 

120 </a> 

121 </div> 

122 ''', 

123 url=create_url, 

124 field_name=field_name, 

125 verbose_name=verbose_name 

126 ) 

127 

128 # Try to inject after the filter input 

129 modified_html = re.sub( 

130 filter_pattern, 

131 r'\1' + str(create_button_html), 

132 original_html, 

133 count=1 

134 ) 

135 

136 # If that didn't work, try to inject after any available objects header 

137 if modified_html == original_html: 

138 modified_html = re.sub( 

139 available_pattern, 

140 r'\1' + str(create_button_html), 

141 original_html, 

142 count=1, 

143 flags=re.IGNORECASE 

144 ) 

145 

146 # If still no match, inject at the beginning of the selector-available div 

147 if modified_html == original_html: 

148 available_div_pattern = r'(<div[^>]*class="[^"]*selector-available[^"]*"[^>]*>)' 

149 modified_html = re.sub( 

150 available_div_pattern, 

151 r'\1' + str(create_button_html), 

152 original_html, 

153 count=1 

154 ) 

155 

156 # Final fallback: wrap the entire widget 

157 if modified_html == original_html: 

158 modified_html = format_html( 

159 ''' 

160 <div class="inline-create-wrapper transfer-widget"> 

161 {create_button} 

162 {original} 

163 </div> 

164 ''', 

165 create_button=create_button_html, 

166 original=original_html 

167 ) 

168 

169 return modified_html 

170 

171 def __getattr__(self, name): 

172 # Delegate attribute access to the wrapped widget 

173 return getattr(self.widget, name) 

174 

175 @property 

176 def media(self): 

177 # Combine original widget media with our custom media 

178 from django import forms 

179 widget_media = getattr(self.widget, 'media', forms.Media()) 

180 custom_media = forms.Media( 

181 css={'all': ('admin/css/inline_create.css',)}, 

182 js=('admin/js/inline_create.js',) 

183 ) 

184 return widget_media + custom_media 

185 

186 return InlineCreateWidgetWrapper(original_widget) 

187 

188 

189class QuickCreateMixin: 

190 """ 

191 Mixin to handle quick creation of related objects via AJAX 

192 """ 

193 

194 def get_urls(self): 

195 from django.urls import path 

196 urls = super().get_urls() 

197 custom_urls = [ 

198 path('quick_create/<str:model_name>/', 

199 self.admin_site.admin_view(self.quick_create_view), 

200 name=f'{self.model._meta.app_label}_{self.model._meta.model_name}_quick_create'), 

201 ] 

202 return custom_urls + urls 

203 

204 def quick_create_view(self, request, model_name): 

205 """ 

206 Handle quick creation of related objects 

207 """ 

208 # Import here to avoid circular imports 

209 from django.apps import apps 

210 

211 try: 

212 model = apps.get_model(self.model._meta.app_label, model_name) 

213 model_admin = self.admin_site._registry.get(model) 

214 

215 if not model_admin: 

216 return JsonResponse({'error': 'Model admin not found'}, status=404) 

217 

218 if request.method == 'POST': 

219 # Handle form submission 

220 form = model_admin.get_form(request)() 

221 form = form.__class__(request.POST) 

222 

223 if form.is_valid(): 

224 obj = form.save() 

225 return JsonResponse({ 

226 'success': True, 

227 'id': obj.pk, 

228 'name': str(obj), 

229 'model': model_name 

230 }) 

231 else: 

232 return JsonResponse({ 

233 'success': False, 

234 'errors': form.errors 

235 }) 

236 

237 else: 

238 # Show form 

239 form = model_admin.get_form(request)() 

240 context = { 

241 'form': form, 

242 'model_name': model_name, 

243 'verbose_name': model._meta.verbose_name, 

244 'opts': model._meta, 

245 } 

246 return render(request, 'admin/quick_create_form.html', context) 

247 

248 except Exception as e: 

249 return JsonResponse({'error': str(e)}, status=500) 

250 

251 

252class FamilyAdminMixin(InlineCreateMixin, QuickCreateMixin): 

253 """ 

254 Combined mixin for family-friendly admin features 

255 """ 

256 pass