Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# (c) 2018-2020 

2# MPIB <https://www.mpib-berlin.mpg.de/>, 

3# MPI-CBS <https://www.cbs.mpg.de/>, 

4# MPIP <http://www.psych.mpg.de/> 

5# 

6# This file is part of Castellum. 

7# 

8# Castellum is free software; you can redistribute it and/or modify it 

9# under the terms of the GNU Affero General Public License as published 

10# by the Free Software Foundation; either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# Castellum is distributed in the hope that it will be useful, but 

14# WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

16# Affero General Public License for more details. 

17# 

18# You should have received a copy of the GNU Affero General Public 

19# License along with Castellum. If not, see 

20# <http://www.gnu.org/licenses/>. 

21 

22from datetime import date 

23 

24from django.db import models 

25from django.urls import reverse 

26from django.utils.functional import cached_property 

27from django.utils.translation import gettext_lazy as _ 

28 

29from dateutil.relativedelta import relativedelta 

30 

31from castellum.subjects.models import Subject 

32from castellum.utils.fields import DateField 

33from castellum.utils.fields import PhoneNumberField 

34from castellum.utils.models import TimeStampedModel 

35 

36from .utils import phonetic 

37 

38 

39class ContactManager(models.Manager): 

40 def fuzzy_filter(self, search): 

41 parts = search.split() 

42 q = self.all_parts_match(parts) & ( 

43 self.full_name_is_matched(parts) | 

44 self.email_is_matched(parts) | 

45 self.guardian_full_name_is_matched(parts) | 

46 self.guardian_email_is_matched(parts) 

47 ) 

48 return self.filter(q).distinct() 

49 

50 def match_name(self, part, prefix): 

51 lookup = prefix + '_name_phonetic__contains' 

52 q = models.Q() 

53 for code in phonetic(part, encode=False): 

54 q &= models.Q(**{lookup: ':{}:'.format(code)}) 

55 return q 

56 

57 def name_is_matched(self, parts, prefix): 

58 q = models.Q() 

59 for part in parts: 

60 if '@' not in part: 

61 q |= self.match_name(part, prefix) 

62 return q 

63 

64 def full_name_is_matched(self, parts): 

65 return self.name_is_matched(parts, 'first') & self.name_is_matched(parts, 'last') 

66 

67 def email_is_matched(self, parts): 

68 q = models.Q() 

69 for part in parts: 

70 if '@' in part: 

71 q |= models.Q(email__iexact=part) 

72 return q 

73 

74 def guardian_full_name_is_matched(self, parts): 

75 return ( 

76 self.name_is_matched(parts, 'guardians__first') & 

77 self.name_is_matched(parts, 'guardians__last') 

78 ) 

79 

80 def guardian_email_is_matched(self, parts): 

81 q = models.Q() 

82 for part in parts: 

83 if '@' in part: 

84 q |= models.Q(guardians__email__iexact=part) 

85 return q 

86 

87 def all_parts_match(self, parts): 

88 q = models.Q() 

89 for part in parts: 

90 if '@' in part: 

91 q &= ( 

92 models.Q(email__iexact=part) | 

93 models.Q(guardians__email__iexact=part) 

94 ) 

95 else: 

96 q &= ( 

97 self.match_name(part, 'first') | 

98 self.match_name(part, 'last') | 

99 self.match_name(part, 'guardians__first') | 

100 self.match_name(part, 'guardians__last') 

101 ) 

102 return q 

103 

104 

105class Contact(TimeStampedModel): 

106 GENDER = ( 

107 ("f", _("female")), 

108 ("m", _("male")), 

109 ("*", _("diverse")), 

110 ) 

111 CONTACT_METHODS = ( 

112 ("phone", _("phone")), 

113 ("email", _("email")), 

114 ("postal", _("postal")), 

115 ) 

116 

117 subject_id = models.PositiveIntegerField(unique=True, editable=False, default=None) 

118 

119 first_name = models.CharField(_('First name'), max_length=64) 

120 first_name_phonetic = models.CharField(max_length=128, editable=False, default=None) 

121 last_name = models.CharField(_('Last name'), max_length=64) 

122 last_name_phonetic = models.CharField(max_length=128, editable=False, default=None) 

123 title = models.CharField(_('Title'), max_length=64, blank=True) 

124 

125 gender = models.CharField(_('Gender'), max_length=1, choices=GENDER, default="f") 

126 

127 date_of_birth = DateField(_('Date of birth'), blank=True, null=True) 

128 

129 email = models.EmailField(_('Email'), max_length=128, blank=True) 

130 phone_number = PhoneNumberField(_('Phone number'), max_length=32, blank=True) 

131 phone_number_alternative = PhoneNumberField( 

132 _('Phone number alternative'), max_length=32, blank=True 

133 ) 

134 

135 preferred_contact_method = models.CharField( 

136 _('Preferred contact method'), max_length=16, blank=True, choices=CONTACT_METHODS 

137 ) 

138 

139 guardians = models.ManyToManyField( 

140 'contacts.Contact', verbose_name=_('Guardians'), related_name='guardian_of', blank=True 

141 ) 

142 

143 objects = ContactManager() 

144 

145 class Meta: 

146 ordering = ('last_name', 'first_name') 

147 verbose_name = _('Contact') 

148 verbose_name_plural = _('Contacts') 

149 

150 def __str__(self): 

151 return self.full_name 

152 

153 def get_address(self): 

154 try: 

155 return self.address 

156 except Address.DoesNotExist: 

157 return None 

158 

159 @cached_property 

160 def subject(self): 

161 subject = Subject.objects.get(pk=self.subject_id) 

162 subject.__dict__['contact'] = self 

163 return subject 

164 

165 @property 

166 def is_complete(self): 

167 return (self.is_guardian or self.date_of_birth) and self.is_reachable 

168 

169 @property 

170 def is_reachable(self): 

171 return any([ 

172 self.get_address(), self.phone_number, self.email, self.has_guardian 

173 ]) 

174 

175 @property 

176 def full_name(self): 

177 return " ".join(filter(None, [self.title, self.first_name, self.last_name])) 

178 

179 @property 

180 def short_name(self): 

181 # a bit simplistic, but should be sufficient 

182 return '%s. %s' % (self.first_name[0], self.last_name) 

183 

184 @property 

185 def age(self): 

186 return relativedelta(date.today(), self.date_of_birth).years 

187 

188 @cached_property 

189 def is_guardian(self): 

190 return self.id and self.guardian_of.exists() 

191 

192 @cached_property 

193 def has_guardian(self): 

194 return self.id and self.guardians.exists() 

195 

196 def save(self, *args, **kwargs): 

197 if not self.subject_id: 

198 subject = Subject.objects.create() 

199 self.subject_id = subject.pk 

200 

201 self.first_name_phonetic = phonetic(self.first_name) 

202 self.last_name_phonetic = phonetic(self.last_name) 

203 

204 return super().save(*args, **kwargs) 

205 

206 def get_absolute_url(self): 

207 return reverse('subjects:detail', args=[self.subject.pk]) 

208 

209 

210class Address(models.Model): 

211 contact = models.OneToOneField(Contact, on_delete=models.CASCADE) 

212 country = models.CharField(_('Country'), max_length=64) 

213 city = models.CharField(_('City'), max_length=128) 

214 zip_code = models.CharField(_('Zip code'), max_length=5) 

215 street = models.CharField(_('Street'), max_length=128) 

216 house_number = models.CharField(_('House number'), max_length=5) 

217 additional_information = models.CharField( 

218 _('Additional information'), max_length=32, blank=True 

219 ) 

220 

221 def lines(self): 

222 line1 = ' '.join([self.street, self.house_number, self.additional_information]).strip() 

223 line2 = '{} {}'.format(self.zip_code, self.city) 

224 return line1, line2 

225 

226 def __str__(self): 

227 return '{}, {}'.format(*self.lines()) 

228 

229 class Meta: 

230 ordering = ( 

231 'country', 'city', 'zip_code', 'street', 'house_number', 'additional_information' 

232 ) 

233 verbose_name = _('Address') 

234 verbose_name_plural = _('Addresses')