Coverage for family/widgets.py: 64%
104 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"""
2Custom widgets for Family Knowledge Management System
3Enhanced form widgets with family-friendly features
4"""
6from django import forms
7from django.forms.widgets import Widget
8from django.urls import reverse
9from django.utils.safestring import mark_safe
10from django.templatetags.static import static
11import json
14class FamilyAutoCompleteWidget(forms.TextInput):
15 """
16 Auto-complete widget for family member names
17 """
18 def __init__(self, model=None, attrs=None):
19 self.model = model
20 default_attrs = {
21 'class': 'family-autocomplete',
22 'autocomplete': 'off',
23 'placeholder': '开始输入姓名...'
24 }
25 if attrs:
26 default_attrs.update(attrs)
27 super().__init__(default_attrs)
29 def format_value(self, value):
30 if value and hasattr(value, '__iter__') and not isinstance(value, str):
31 return ', '.join(str(v) for v in value)
32 return super().format_value(value)
34 class Media:
35 css = {
36 'all': ('admin/css/family_autocomplete.css',)
37 }
38 js = ('admin/js/family_autocomplete.js',)
41class LocationAutoCompleteWidget(forms.TextInput):
42 """
43 Auto-complete widget for locations
44 """
45 def __init__(self, attrs=None):
46 default_attrs = {
47 'class': 'location-autocomplete',
48 'autocomplete': 'off',
49 'placeholder': '输入地点名称...'
50 }
51 if attrs: 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 default_attrs.update(attrs)
53 super().__init__(default_attrs)
55 class Media:
56 css = {
57 'all': ('admin/css/family_autocomplete.css',)
58 }
59 js = ('admin/js/location_autocomplete.js',)
62class InstitutionAutoCompleteWidget(forms.TextInput):
63 """
64 Auto-complete widget for institutions (hospitals, schools, companies)
65 """
66 def __init__(self, institution_type=None, attrs=None):
67 self.institution_type = institution_type
68 default_attrs = {
69 'class': 'institution-autocomplete',
70 'autocomplete': 'off',
71 'placeholder': '输入机构名称...',
72 'data-institution-type': institution_type or 'all'
73 }
74 if attrs: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
75 default_attrs.update(attrs)
76 super().__init__(default_attrs)
78 class Media:
79 css = {
80 'all': ('admin/css/family_autocomplete.css',)
81 }
82 js = ('admin/js/institution_autocomplete.js',)
85class FamilyDateWidget(forms.DateInput):
86 """
87 Enhanced date picker with quick options
88 """
89 def __init__(self, attrs=None):
90 default_attrs = {
91 'class': 'family-date-picker',
92 'type': 'date'
93 }
94 if attrs: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 default_attrs.update(attrs)
96 super().__init__(default_attrs)
98 def render(self, name, value, attrs=None, renderer=None):
99 html = super().render(name, value, attrs, renderer)
101 complete_html = f'''
102 <div class="family-date-container" data-field-name="{name}">
103 {html}
104 <div class="quick-dates-wrapper">
105 <div class="quick-dates">
106 <button type="button" class="quick-date-btn" data-days="0" onclick="setFamilyDate(this, '{name}', 0)">今天</button>
107 <button type="button" class="quick-date-btn" data-days="-1" onclick="setFamilyDate(this, '{name}', -1)">昨天</button>
108 <button type="button" class="quick-date-btn" data-days="-7" onclick="setFamilyDate(this, '{name}', -7)">一周前</button>
109 <button type="button" class="quick-date-btn" data-days="-30" onclick="setFamilyDate(this, '{name}', -30)">一月前</button>
110 <button type="button" class="quick-date-btn" data-clear="true" onclick="clearFamilyDate(this, '{name}')">清除</button>
111 </div>
112 </div>
113 </div>
114 '''
116 return mark_safe(complete_html)
118 class Media:
119 css = {
120 'all': ('admin/css/family_date_widget.css',)
121 }
122 js = ('admin/js/family_date_widget.js',)
125class FamilyPhotoWidget(forms.ClearableFileInput):
126 """
127 Enhanced photo upload widget with preview and drag-drop
128 """
130 def __init__(self, attrs=None):
131 default_attrs = {
132 'class': 'family-photo-upload',
133 'accept': 'image/*',
134 'multiple': False
135 }
136 if attrs: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true
137 default_attrs.update(attrs)
138 super().__init__(default_attrs)
140 def render(self, name, value, attrs=None, renderer=None):
141 # Get the base file input
142 input_html = super().render(name, value, attrs, renderer)
144 # Add preview and drag-drop functionality
145 preview_html = '''
146 <div class="photo-upload-wrapper">
147 <div class="photo-drop-zone" ondrop="handlePhotoDrop(event)" ondragover="handlePhotoDragOver(event)" ondragleave="handlePhotoDragLeave(event)">
148 <div class="drop-zone-content">
149 <div class="upload-icon">📸</div>
150 <div class="upload-text">
151 <p><strong>点击选择照片</strong> 或 拖拽照片到这里</p>
152 <p class="upload-hint">支持 JPG, PNG, GIF 格式</p>
153 </div>
154 </div>
155 <div class="photo-preview" style="display: none;">
156 <img class="preview-image" src="" alt="预览">
157 <div class="preview-info">
158 <span class="file-name"></span>
159 <span class="file-size"></span>
160 </div>
161 <button type="button" class="remove-photo" onclick="removePhotoPreview(this)">×</button>
162 </div>
163 </div>
164 </div>
165 '''
167 return mark_safe(preview_html + input_html)
169 class Media:
170 css = {
171 'all': ('admin/css/family_photo_widget.css',)
172 }
173 js = ('admin/js/family_photo_widget.js',)
176class RelationshipSelectorWidget(forms.Select):
177 """
178 Visual relationship selector with family tree interface
179 """
180 def __init__(self, attrs=None):
181 default_attrs = {
182 'class': 'relationship-selector',
183 'size': '6'
184 }
185 if attrs: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 default_attrs.update(attrs)
187 super().__init__(default_attrs)
189 def render(self, name, value, attrs=None, renderer=None):
190 # Get the base select widget
191 select_html = super().render(name, value, attrs, renderer)
193 # Clean single-select relationship interface
194 visual_html = f'''
195 <div class="relationship-widget-container">
196 <div class="relationship-header">
197 <div class="header-left">
198 <span class="widget-title">关系类型</span>
199 <div class="selected-display">
200 <span class="summary-label">已选择:</span>
201 <span class="current-selection" id="current-selection-{name}">未选择</span>
202 </div>
203 </div>
204 <button type="button" class="clear-selection-btn" onclick="clearSelection('{name}')" title="清除选择">
205 <span>清除</span>
206 </button>
207 </div>
209 <div class="relationship-categories">
210 <div class="relationship-section blood-relations">
211 <div class="section-header">
212 <span class="section-icon">👨👩👧👦</span>
213 <h4>血缘关系</h4>
214 </div>
215 <div class="relation-grid">
216 <button type="button" class="relation-btn blood" data-relation="父子">父子</button>
217 <button type="button" class="relation-btn blood" data-relation="母子">母子</button>
218 <button type="button" class="relation-btn blood" data-relation="父女">父女</button>
219 <button type="button" class="relation-btn blood" data-relation="母女">母女</button>
220 <button type="button" class="relation-btn blood" data-relation="兄弟">兄弟</button>
221 <button type="button" class="relation-btn blood" data-relation="姐妹">姐妹</button>
222 </div>
223 </div>
225 <div class="relationship-section marriage-relations">
226 <div class="section-header">
227 <span class="section-icon">💑</span>
228 <h4>姻亲关系</h4>
229 </div>
230 <div class="relation-grid">
231 <button type="button" class="relation-btn marriage" data-relation="夫妻">夫妻</button>
232 <button type="button" class="relation-btn marriage" data-relation="翁婿">翁婿</button>
233 <button type="button" class="relation-btn marriage" data-relation="姑嫂">姑嫂</button>
234 <button type="button" class="relation-btn marriage" data-relation="连襟">连襟</button>
235 </div>
236 </div>
238 <div class="relationship-section other-relations">
239 <div class="section-header">
240 <span class="section-icon">👥</span>
241 <h4>其他关系</h4>
242 </div>
243 <div class="relation-grid">
244 <button type="button" class="relation-btn other" data-relation="朋友">朋友</button>
245 <button type="button" class="relation-btn other" data-relation="同事">同事</button>
246 <button type="button" class="relation-btn other" data-relation="邻居">邻居</button>
247 <button type="button" class="relation-btn other" data-relation="其他">其他</button>
248 </div>
249 </div>
250 </div>
252 <!-- Hidden select for form submission -->
253 <div class="hidden-select-wrapper" style="display: none;">
254 {select_html}
255 </div>
256 </div>
257 '''
259 return mark_safe(visual_html)
261 class Media:
262 css = {
263 'all': ('admin/css/relationship_widget.css',)
264 }
265 js = ('admin/js/relationship_widget.js',)
268class RichTextWidget(forms.Textarea):
269 """
270 Simple rich text editor for story content
271 """
272 def __init__(self, attrs=None):
273 default_attrs = {
274 'class': 'rich-text-editor',
275 'rows': 8
276 }
277 if attrs: 277 ↛ 279line 277 didn't jump to line 279 because the condition on line 277 was always true
278 default_attrs.update(attrs)
279 super().__init__(default_attrs)
281 def render(self, name, value, attrs=None, renderer=None):
282 textarea_html = super().render(name, value, attrs, renderer)
284 complete_html = f'''
285 <div class="rich-text-container">
286 <div class="rich-text-toolbar">
287 <div class="toolbar-group">
288 <button type="button" class="toolbar-btn" data-command="bold" title="粗体">
289 <strong>B</strong>
290 </button>
291 <button type="button" class="toolbar-btn" data-command="italic" title="斜体">
292 <em>I</em>
293 </button>
294 <button type="button" class="toolbar-btn" data-command="underline" title="下划线">
295 <u>U</u>
296 </button>
297 </div>
298 <div class="toolbar-group">
299 <button type="button" class="toolbar-btn" data-command="heading" title="标题">
300 H
301 </button>
302 <button type="button" class="toolbar-btn" data-command="paragraph" title="段落">
303 P
304 </button>
305 </div>
306 <div class="toolbar-group">
307 <button type="button" class="toolbar-btn" data-command="quote" title="引用">
308 "
309 </button>
310 <button type="button" class="toolbar-btn" data-command="list" title="列表">
311 •
312 </button>
313 </div>
314 <div class="toolbar-group">
315 <button type="button" class="toolbar-btn" data-command="photo" title="插入照片">
316 📷
317 </button>
318 <button type="button" class="toolbar-btn" data-command="emoji" title="表情">
319 😊
320 </button>
321 </div>
322 </div>
323 {textarea_html}
324 </div>
325 '''
327 return mark_safe(complete_html)
329 class Media:
330 css = {
331 'all': ('admin/css/rich_text_widget.css',)
332 }
333 js = ('admin/js/rich_text_widget.js',)
336class TagsWidget(forms.TextInput):
337 """
338 Widget for entering tags with auto-suggestions
339 """
340 def __init__(self, attrs=None):
341 default_attrs = {
342 'class': 'tags-input',
343 'placeholder': '输入标签,用逗号分隔...'
344 }
345 if attrs:
346 default_attrs.update(attrs)
347 super().__init__(default_attrs)
349 def render(self, name, value, attrs=None, renderer=None):
350 input_html = super().render(name, value, attrs, renderer)
352 tags_html = '''
353 <div class="tags-wrapper">
354 <div class="tags-container"></div>
355 <div class="tags-suggestions">
356 <div class="suggestion-category">
357 <h5>常用标签</h5>
358 <div class="tag-suggestions">
359 <span class="tag-suggestion">生日</span>
360 <span class="tag-suggestion">节日</span>
361 <span class="tag-suggestion">旅行</span>
362 <span class="tag-suggestion">聚会</span>
363 <span class="tag-suggestion">成长</span>
364 <span class="tag-suggestion">纪念</span>
365 </div>
366 </div>
367 <div class="suggestion-category">
368 <h5>情感标签</h5>
369 <div class="tag-suggestions">
370 <span class="tag-suggestion">温馨</span>
371 <span class="tag-suggestion">感动</span>
372 <span class="tag-suggestion">快乐</span>
373 <span class="tag-suggestion">怀念</span>
374 <span class="tag-suggestion">骄傲</span>
375 <span class="tag-suggestion">感恩</span>
376 </div>
377 </div>
378 </div>
379 </div>
380 '''
382 return mark_safe(tags_html + input_html)
384 class Media:
385 css = {
386 'all': ('admin/css/tags_widget.css',)
387 }
388 js = ('admin/js/tags_widget.js',)