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/>.
22from datetime import date
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 _
29from dateutil.relativedelta import relativedelta
31from castellum.subjects.models import Subject
32from castellum.utils.fields import DateField
33from castellum.utils.fields import PhoneNumberField
34from castellum.utils.models import TimeStampedModel
36from .utils import phonetic
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()
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
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
64 def full_name_is_matched(self, parts):
65 return self.name_is_matched(parts, 'first') & self.name_is_matched(parts, 'last')
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
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 )
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
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
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 )
117 subject_id = models.PositiveIntegerField(unique=True, editable=False, default=None)
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)
125 gender = models.CharField(_('Gender'), max_length=1, choices=GENDER, default="f")
127 date_of_birth = DateField(_('Date of birth'), blank=True, null=True)
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 )
135 preferred_contact_method = models.CharField(
136 _('Preferred contact method'), max_length=16, blank=True, choices=CONTACT_METHODS
137 )
139 guardians = models.ManyToManyField(
140 'contacts.Contact', verbose_name=_('Guardians'), related_name='guardian_of', blank=True
141 )
143 objects = ContactManager()
145 class Meta:
146 ordering = ('last_name', 'first_name')
147 verbose_name = _('Contact')
148 verbose_name_plural = _('Contacts')
150 def __str__(self):
151 return self.full_name
153 def get_address(self):
154 try:
155 return self.address
156 except Address.DoesNotExist:
157 return None
159 @cached_property
160 def subject(self):
161 subject = Subject.objects.get(pk=self.subject_id)
162 subject.__dict__['contact'] = self
163 return subject
165 @property
166 def is_complete(self):
167 return (self.is_guardian or self.date_of_birth) and self.is_reachable
169 @property
170 def is_reachable(self):
171 return any([
172 self.get_address(), self.phone_number, self.email, self.has_guardian
173 ])
175 @property
176 def full_name(self):
177 return " ".join(filter(None, [self.title, self.first_name, self.last_name]))
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)
184 @property
185 def age(self):
186 return relativedelta(date.today(), self.date_of_birth).years
188 @cached_property
189 def is_guardian(self):
190 return self.id and self.guardian_of.exists()
192 @cached_property
193 def has_guardian(self):
194 return self.id and self.guardians.exists()
196 def save(self, *args, **kwargs):
197 if not self.subject_id:
198 subject = Subject.objects.create()
199 self.subject_id = subject.pk
201 self.first_name_phonetic = phonetic(self.first_name)
202 self.last_name_phonetic = phonetic(self.last_name)
204 return super().save(*args, **kwargs)
206 def get_absolute_url(self):
207 return reverse('subjects:detail', args=[self.subject.pk])
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 )
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
226 def __str__(self):
227 return '{}, {}'.format(*self.lines())
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')