Coverage for family/widgets.py: 64%

104 statements  

« 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""" 

5 

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 

12 

13 

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) 

28 

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) 

33 

34 class Media: 

35 css = { 

36 'all': ('admin/css/family_autocomplete.css',) 

37 } 

38 js = ('admin/js/family_autocomplete.js',) 

39 

40 

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) 

54 

55 class Media: 

56 css = { 

57 'all': ('admin/css/family_autocomplete.css',) 

58 } 

59 js = ('admin/js/location_autocomplete.js',) 

60 

61 

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) 

77 

78 class Media: 

79 css = { 

80 'all': ('admin/css/family_autocomplete.css',) 

81 } 

82 js = ('admin/js/institution_autocomplete.js',) 

83 

84 

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) 

97 

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

99 html = super().render(name, value, attrs, renderer) 

100 

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 ''' 

115 

116 return mark_safe(complete_html) 

117 

118 class Media: 

119 css = { 

120 'all': ('admin/css/family_date_widget.css',) 

121 } 

122 js = ('admin/js/family_date_widget.js',) 

123 

124 

125class FamilyPhotoWidget(forms.ClearableFileInput): 

126 """ 

127 Enhanced photo upload widget with preview and drag-drop 

128 """ 

129 

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) 

139 

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) 

143 

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 ''' 

166 

167 return mark_safe(preview_html + input_html) 

168 

169 class Media: 

170 css = { 

171 'all': ('admin/css/family_photo_widget.css',) 

172 } 

173 js = ('admin/js/family_photo_widget.js',) 

174 

175 

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) 

188 

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) 

192 

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> 

208  

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> 

224  

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> 

237  

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> 

251  

252 <!-- Hidden select for form submission --> 

253 <div class="hidden-select-wrapper" style="display: none;"> 

254 {select_html} 

255 </div> 

256 </div> 

257 ''' 

258 

259 return mark_safe(visual_html) 

260 

261 class Media: 

262 css = { 

263 'all': ('admin/css/relationship_widget.css',) 

264 } 

265 js = ('admin/js/relationship_widget.js',) 

266 

267 

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) 

280 

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

282 textarea_html = super().render(name, value, attrs, renderer) 

283 

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 ''' 

326 

327 return mark_safe(complete_html) 

328 

329 class Media: 

330 css = { 

331 'all': ('admin/css/rich_text_widget.css',) 

332 } 

333 js = ('admin/js/rich_text_widget.js',) 

334 

335 

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) 

348 

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

350 input_html = super().render(name, value, attrs, renderer) 

351 

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 ''' 

381 

382 return mark_safe(tags_html + input_html) 

383 

384 class Media: 

385 css = { 

386 'all': ('admin/css/tags_widget.css',) 

387 } 

388 js = ('admin/js/tags_widget.js',)