Coverage for family/forms.py: 33%
152 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 forms for Family Knowledge Management System
3Enhanced forms with family-friendly widgets and validation
4"""
6from django import forms
7from django.core.exceptions import ValidationError
8from django.utils import timezone
9from datetime import date, timedelta
10from .models import (
11 Person, Story, Event, Multimedia, Relationship,
12 Location, Institution, Health, Heritage, Planning
13)
14from .widgets import (
15 FamilyAutoCompleteWidget, LocationAutoCompleteWidget,
16 InstitutionAutoCompleteWidget, FamilyDateWidget,
17 FamilyPhotoWidget, RelationshipSelectorWidget,
18 RichTextWidget, TagsWidget
19)
22class PersonAdminForm(forms.ModelForm):
23 """
24 Enhanced form for Person model with family-friendly widgets
25 """
27 class Meta:
28 model = Person
29 fields = '__all__'
30 widgets = {
31 'birth_date': FamilyDateWidget(),
32 'death_date': FamilyDateWidget(),
33 'photo': FamilyPhotoWidget(),
34 'bio': RichTextWidget(attrs={'rows': 4}),
35 }
37 def __init__(self, *args, **kwargs):
38 super().__init__(*args, **kwargs)
40 # Add helpful placeholders and labels
41 self.fields['name'].widget.attrs.update({
42 'placeholder': '请输入姓名',
43 'class': 'form-control'
44 })
46 self.fields['gender'].widget.attrs.update({
47 'class': 'form-control'
48 })
50 self.fields['bio'].widget.attrs.update({
51 'placeholder': '在这里记录关于这个人的信息...'
52 })
54 # Add age calculation help text
55 if self.instance and self.instance.birth_date:
56 age = self.calculate_age(self.instance.birth_date)
57 self.fields['birth_date'].help_text = f'当前年龄: {age}岁'
59 def calculate_age(self, birth_date):
60 """Calculate age from birth date"""
61 today = date.today()
62 return today.year - birth_date.year - ((today.month, today.day) < (birth_date.month, birth_date.day))
64 def clean_birth_date(self):
65 birth_date = self.cleaned_data.get('birth_date')
66 if birth_date and birth_date > date.today():
67 raise ValidationError('出生日期不能是未来的日期')
68 return birth_date
70 def clean_death_date(self):
71 death_date = self.cleaned_data.get('death_date')
72 birth_date = self.cleaned_data.get('birth_date')
74 if death_date:
75 if death_date > date.today():
76 raise ValidationError('逝世日期不能是未来的日期')
77 if birth_date and death_date < birth_date:
78 raise ValidationError('逝世日期不能早于出生日期')
80 return death_date
83class StoryAdminForm(forms.ModelForm):
84 """
85 Enhanced form for Story model with rich text editing
86 """
88 class Meta:
89 model = Story
90 fields = '__all__'
91 widgets = {
92 'content': RichTextWidget(attrs={'rows': 8}),
93 'date_occurred': FamilyDateWidget(),
94 'location': LocationAutoCompleteWidget(),
95 }
97 def __init__(self, *args, **kwargs):
98 super().__init__(*args, **kwargs)
100 self.fields['title'].widget.attrs.update({
101 'placeholder': '给这个故事起个温暖的标题...',
102 'class': 'form-control'
103 })
105 self.fields['content'].widget.attrs.update({
106 'placeholder': '在这里记录您的家族故事...\n\n例如:\n• 那个难忘的春节聚会\n• 爷爷教我下棋的午后\n• 全家一起包饺子的温馨时光'
107 })
109 # Set default date to today
110 if not self.instance.pk:
111 self.fields['date_occurred'].initial = date.today()
114class EventAdminForm(forms.ModelForm):
115 """
116 Enhanced form for Event model with smart date handling
117 """
119 class Meta:
120 model = Event
121 fields = '__all__'
122 widgets = {
123 'start_date': FamilyDateWidget(),
124 'end_date': FamilyDateWidget(),
125 'location': LocationAutoCompleteWidget(),
126 'description': RichTextWidget(attrs={'rows': 4}),
127 }
129 def __init__(self, *args, **kwargs):
130 super().__init__(*args, **kwargs)
132 self.fields['name'].widget.attrs.update({
133 'placeholder': '活动名称,如:小明生日聚会',
134 'class': 'form-control'
135 })
137 self.fields['event_type'].widget.attrs.update({
138 'class': 'form-control'
139 })
141 # Add future date validation help
142 self.fields['start_date'].help_text = '可以是过去、今天或未来的日期'
144 def clean_start_date(self):
145 start_date = self.cleaned_data.get('start_date')
146 if start_date:
147 # Allow past, present and future dates for events
148 # But warn for very old dates
149 if start_date.date() < date.today() - timedelta(days=365*10):
150 # This is just a warning, not an error
151 pass
152 return start_date
155class MultimediaAdminForm(forms.ModelForm):
156 """
157 Enhanced form for Multimedia model with photo preview
158 """
160 class Meta:
161 model = Multimedia
162 fields = '__all__'
163 widgets = {
164 'file': FamilyPhotoWidget(),
165 'created_date': FamilyDateWidget(),
166 'location': LocationAutoCompleteWidget(),
167 'description': RichTextWidget(attrs={'rows': 3}),
168 }
170 def __init__(self, *args, **kwargs):
171 super().__init__(*args, **kwargs)
173 self.fields['title'].widget.attrs.update({
174 'placeholder': '照片或视频的标题...'
175 })
177 self.fields['description'].widget.attrs.update({
178 'placeholder': '描述这张照片的故事...'
179 })
181 # Set default upload date to today
182 if not self.instance.pk:
183 self.fields['created_date'].initial = timezone.now()
185 def clean_file(self):
186 file = self.cleaned_data.get('file')
187 if file:
188 # Check file size (max 10MB)
189 if file.size > 10 * 1024 * 1024:
190 raise ValidationError('文件大小不能超过10MB')
192 # Check file type
193 allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'video/mp4', 'video/mov']
194 if hasattr(file, 'content_type') and file.content_type not in allowed_types:
195 raise ValidationError('只支持 JPG, PNG, GIF 图片和 MP4, MOV 视频格式')
197 return file
200class RelationshipAdminForm(forms.ModelForm):
201 """
202 Enhanced form for Relationship model with visual selector
203 """
205 class Meta:
206 model = Relationship
207 fields = '__all__'
208 widgets = {
209 'relationship_type': RelationshipSelectorWidget(),
210 'start_date': FamilyDateWidget(),
211 'end_date': FamilyDateWidget(),
212 'description': RichTextWidget(attrs={'rows': 3}),
213 }
215 def __init__(self, *args, **kwargs):
216 super().__init__(*args, **kwargs)
218 # Add help text for relationship fields
219 self.fields['person_from'].help_text = '选择关系中的第一个人'
220 self.fields['person_to'].help_text = '选择关系中的第二个人'
221 self.fields['relationship_type'].help_text = '选择他们之间的关系类型'
223 def clean(self):
224 cleaned_data = super().clean()
225 person_from = cleaned_data.get('person_from')
226 person_to = cleaned_data.get('person_to')
228 if person_from and person_to and person_from == person_to:
229 raise ValidationError('不能建立一个人与自己的关系')
231 # Check for duplicate relationships
232 if person_from and person_to:
233 existing = Relationship.objects.filter(
234 person_from=person_from, person_to=person_to
235 ).exclude(pk=self.instance.pk if self.instance else None)
237 if existing.exists():
238 raise ValidationError('这两个人之间已经存在关系记录')
240 return cleaned_data
243class HealthAdminForm(forms.ModelForm):
244 """
245 Enhanced form for Health model with privacy considerations
246 """
248 class Meta:
249 model = Health
250 fields = '__all__'
251 widgets = {
252 'date': FamilyDateWidget(),
253 'institution': InstitutionAutoCompleteWidget(institution_type='hospital'),
254 'description': RichTextWidget(attrs={'rows': 4}),
255 }
257 def __init__(self, *args, **kwargs):
258 super().__init__(*args, **kwargs)
260 # Add privacy notice
261 self.fields['description'].help_text = '健康信息将被安全保护,仅限家庭成员查看'
263 # Set default date to today
264 if not self.instance.pk:
265 self.fields['date'].initial = date.today()
268class LocationAdminForm(forms.ModelForm):
269 """
270 Enhanced form for Location model
271 """
273 class Meta:
274 model = Location
275 fields = '__all__'
276 widgets = {
277 'description': RichTextWidget(attrs={'rows': 3}),
278 }
280 def __init__(self, *args, **kwargs):
281 super().__init__(*args, **kwargs)
283 self.fields['name'].widget.attrs.update({
284 'placeholder': '地点名称,如:北京市朝阳区'
285 })
287 self.fields['address'].widget.attrs.update({
288 'placeholder': '详细地址(可选)'
289 })
292class InstitutionAdminForm(forms.ModelForm):
293 """
294 Enhanced form for Institution model
295 """
297 class Meta:
298 model = Institution
299 fields = '__all__'
300 widgets = {
301 'location': LocationAutoCompleteWidget(),
302 'description': RichTextWidget(attrs={'rows': 3}),
303 }
305 def __init__(self, *args, **kwargs):
306 super().__init__(*args, **kwargs)
308 self.fields['name'].widget.attrs.update({
309 'placeholder': '机构名称,如:北京协和医院'
310 })
312 self.fields['institution_type'].widget.attrs.update({
313 'class': 'form-control'
314 })
317# Helper functions for form validation
319def validate_family_photo(image):
320 """Validate uploaded family photos"""
321 # Check file size
322 if image.size > 5 * 1024 * 1024: # 5MB
323 raise ValidationError('照片文件大小不能超过5MB')
325 # Check image dimensions
326 from PIL import Image
327 img = Image.open(image)
328 if img.width < 100 or img.height < 100:
329 raise ValidationError('照片尺寸太小,请上传至少100x100像素的图片')
331 if img.width > 4000 or img.height > 4000:
332 raise ValidationError('照片尺寸太大,请上传小于4000x4000像素的图片')
335def validate_chinese_name(name):
336 """Validate Chinese names"""
337 import re
338 if not re.match(r'^[\u4e00-\u9fff\w\s]+$', name):
339 raise ValidationError('姓名只能包含中文、英文、数字和空格')
341 if len(name.strip()) < 2:
342 raise ValidationError('姓名至少需要2个字符')
344 if len(name.strip()) > 20:
345 raise ValidationError('姓名不能超过20个字符')
348# Form field choices with Chinese labels
349GENDER_CHOICES = [
350 ('', '请选择性别'),
351 ('male', '男性'),
352 ('female', '女性'),
353 ('other', '其他'),
354]
356EVENT_TYPE_CHOICES = [
357 ('', '请选择事件类型'),
358 ('birthday', '🎂 生日'),
359 ('anniversary', '💒 纪念日'),
360 ('graduation', '🎓 毕业'),
361 ('wedding', '💍 婚礼'),
362 ('travel', '✈️ 旅行'),
363 ('festival', '🎊 节日'),
364 ('achievement', '🏆 成就'),
365 ('family_gathering', '👨👩👧👦 家庭聚会'),
366 ('other', '📝 其他'),
367]
369RELATIONSHIP_TYPE_CHOICES = [
370 ('', '请选择关系类型'),
371 ('parent', '父母'),
372 ('child', '子女'),
373 ('sibling', '兄弟姐妹'),
374 ('spouse', '配偶'),
375 ('grandparent', '祖父母/外祖父母'),
376 ('grandchild', '孙子女/外孙子女'),
377 ('uncle_aunt', '叔伯/姑姨'),
378 ('cousin', '堂兄弟姐妹/表兄弟姐妹'),
379 ('friend', '朋友'),
380 ('colleague', '同事'),
381 ('neighbor', '邻居'),
382 ('other', '其他'),
383]