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
« 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"""
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
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 """
20 # Define which fields should have inline creation buttons
21 inline_create_fields = []
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)
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 )
35 return formfield
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)
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 )
49 return formfield
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
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
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)
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))
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 )
99 return create_button
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
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"[^>]*>)'
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 )
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 )
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 )
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 )
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 )
169 return modified_html
171 def __getattr__(self, name):
172 # Delegate attribute access to the wrapped widget
173 return getattr(self.widget, name)
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
186 return InlineCreateWidgetWrapper(original_widget)
189class QuickCreateMixin:
190 """
191 Mixin to handle quick creation of related objects via AJAX
192 """
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
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
211 try:
212 model = apps.get_model(self.model._meta.app_label, model_name)
213 model_admin = self.admin_site._registry.get(model)
215 if not model_admin:
216 return JsonResponse({'error': 'Model admin not found'}, status=404)
218 if request.method == 'POST':
219 # Handle form submission
220 form = model_admin.get_form(request)()
221 form = form.__class__(request.POST)
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 })
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)
248 except Exception as e:
249 return JsonResponse({'error': str(e)}, status=500)
252class FamilyAdminMixin(InlineCreateMixin, QuickCreateMixin):
253 """
254 Combined mixin for family-friendly admin features
255 """
256 pass