models.py 12.4 KB
Newer Older
1
"""The models defined by the activemembers package"""
Thom Wiggers's avatar
Thom Wiggers committed
2
import datetime
3
import logging
Thom Wiggers's avatar
Thom Wiggers committed
4

5
from django.contrib.auth.models import Permission
6
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
7
from django.core.validators import MinValueValidator
Thom Wiggers's avatar
Thom Wiggers committed
8
from django.db import models
Thom Wiggers's avatar
Thom Wiggers committed
9
from django.urls import reverse
10
from django.utils import timezone
Thom Wiggers's avatar
Thom Wiggers committed
11
from django.utils.translation import ugettext_lazy as _
12
from tinymce import HTMLField
13

14
15
from utils.translation import (ModelTranslateMeta, MultilingualField,
                               localize_attr_name)
Thom Wiggers's avatar
Thom Wiggers committed
16

17
18
19
logger = logging.getLogger(__name__)


20
21
class ActiveMemberGroupManager(models.Manager):
    """Returns active objects only sorted by the localized name"""
22

Thom Wiggers's avatar
Thom Wiggers committed
23
    def get_queryset(self):
Thom Wiggers's avatar
Thom Wiggers committed
24
        return (super().get_queryset()
25
26
                .exclude(active=False)
                .order_by(localize_attr_name('name')))
Thom Wiggers's avatar
Thom Wiggers committed
27
28


29
class MemberGroup(models.Model, metaclass=ModelTranslateMeta):
30
    """Describes a groups of members"""
Thom Wiggers's avatar
Thom Wiggers committed
31

32
    objects = models.Manager()
33
    active_objects = ActiveMemberGroupManager()
Thom Wiggers's avatar
Thom Wiggers committed
34

35
36
    name = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
37
        max_length=40,
38
        verbose_name=_('Name'),
Thom Wiggers's avatar
Thom Wiggers committed
39
40
41
        unique=True,
    )

42
    description = MultilingualField(
43
        HTMLField,
Thom Wiggers's avatar
Thom Wiggers committed
44
45
46
47
48
        verbose_name=_('Description'),
    )

    photo = models.ImageField(
        verbose_name=_('Image'),
Thom Wiggers's avatar
Thom Wiggers committed
49
        upload_to='public/committeephotos/',
Thom Wiggers's avatar
Thom Wiggers committed
50
51
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
52
53
54
    )

    members = models.ManyToManyField(
55
        'members.Member',
56
        through='activemembers.MemberGroupMembership'
Thom Wiggers's avatar
Thom Wiggers committed
57
58
59
60
61
62
63
64
    )

    permissions = models.ManyToManyField(
        Permission,
        verbose_name=_('permissions'),
        blank=True,
    )

Thom Wiggers's avatar
Thom Wiggers committed
65
66
67
68
69
    since = models.DateField(
        _('founded in'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
70

Thom Wiggers's avatar
Thom Wiggers committed
71
72
73
74
75
    until = models.DateField(
        _('existed until'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
76

77
78
79
80
81
82
    active = models.BooleanField(
        default=False,
        help_text=_("This should only be unchecked if the committee has been "
                    "dissolved. The websites assumes that any committees on it"
                    " existed at some point."),
    )
83

84
85
86
87
88
89
    contact_email = models.EmailField(
        _('contact email address'),
        blank=True,
        null=True,
    )

90
91
    contact_mailinglist = models.OneToOneField(
        'mailinglists.MailingList',
92
        verbose_name=_('contact mailing list'),
93
94
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
95
        on_delete=models.SET_NULL,
96
    )
Thom Wiggers's avatar
Thom Wiggers committed
97

98
99
100
101
    display_members = models.BooleanField(
        default=False,
    )

102
103
104
105
106
107
    @property
    def contact_address(self):
        if self.contact_mailinglist:
            return f"{self.contact_mailinglist.name}@thalia.nu"
        return self.contact_email

108
109
110
111
112
113
114
115
116
117
118
119
120
121
    def clean(self):
        if ((self.contact_email is not None and
                self.contact_mailinglist is not None) or
            (self.contact_email is None and
                self.contact_mailinglist is None)):
            raise ValidationError({
                'contact_email':
                    _("Please use either the mailing list "
                      "or email address option."),
                'contact_mailinglist':
                    _("Please use either the mailing list "
                      "or email address option.")
            })

Thom Wiggers's avatar
Thom Wiggers committed
122
123
124
    def __str__(self):
        return self.name

125
126
127
128
129
130
131
    def get_absolute_url(self):
        try:
            return self.board.get_absolute_url()
        except self.DoesNotExist:
            try:
                return self.committee.get_absolute_url()
            except self.DoesNotExist:
Thijs de Jong's avatar
Thijs de Jong committed
132
133
134
135
                try:
                    return self.society.get_absolute_url()
                except self.DoesNotExist:
                    pass
136

Thom Wiggers's avatar
Thom Wiggers committed
137
    class Meta:
138
139
        verbose_name = _('member group')
        verbose_name_plural = _('member groups')
140
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
141
142


143
class Committee(MemberGroup):
144
145
146
147
148
    """Describes a committee, which is a type of MemberGroup"""

    objects = models.Manager()
    active_objects = ActiveMemberGroupManager()

149
150
151
152
153
    wiki_namespace = models.CharField(
        _('Wiki namespace'),
        null=True,
        blank=True,
        max_length=50)
Thom Wiggers's avatar
Thom Wiggers committed
154

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
155
156
157
    def get_absolute_url(self):
        return reverse('activemembers:committee', args=[str(self.pk)])

158
159
160
161
    class Meta:
        verbose_name = _('committee')
        verbose_name_plural = _('committees')
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
162
163


164
class Society(MemberGroup):
165
166
167
168
169
    """Describes a society, which is a type of MemberGroup"""

    objects = models.Manager()
    active_objects = ActiveMemberGroupManager()

Sébastiaan Versteeg's avatar
Sébastiaan Versteeg committed
170
171
172
    def get_absolute_url(self):
        return reverse('activemembers:society', args=[str(self.pk)])

173
174
175
176
    class Meta:
        verbose_name = _('society')
        verbose_name_plural = _('societies')
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
177
178


179
class Board(MemberGroup):
180
181
    """Describes a board, which is a type of MemberGroup"""

182
    class Meta:
183
184
        verbose_name = _('board')
        verbose_name_plural = _('boards')
185
        ordering = ['-since']
186
187
188
        permissions = (
            ('board_wiki', _("Access the board wiki")),
        )
189

190
191
192
193
    def save(self, *args, **kwargs):
        self.active = True
        super().save(*args, **kwargs)

Thom Wiggers's avatar
Thom Wiggers committed
194
    def get_absolute_url(self):
195
196
        return reverse('activemembers:board', args=[str(self.since.year),
                                                    str(self.until.year)])
197
198
199

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
200
201
202
203
204
205
206
        boards = Board.objects.all()
        if self.since is not None:
            for board in boards:
                if board.pk == self.pk:
                    continue
                if ((board.until is None and (
                        self.until is None or self.until >= board.since)) or
207
208
209
210
                        (self.until is None and self.since <= board.until) or
                        (self.until and board.until and
                            self.since <= board.until and
                            self.until >= board.since)):
211
212
213
                    raise ValidationError({
                        'since': _('A board already exists for those years'),
                        'until': _('A board already exists for those years')})
Thom Wiggers's avatar
Thom Wiggers committed
214

Thom Wiggers's avatar
Thom Wiggers committed
215

Thom Wiggers's avatar
Thom Wiggers committed
216
class ActiveMembershipManager(models.Manager):
217
    """
218
    Custom manager that gets the currently active membergroup memberships
219
    """
220

Thom Wiggers's avatar
Thom Wiggers committed
221
    def get_queryset(self):
222
        return super().get_queryset().exclude(until__lt=timezone.now().date())
Thom Wiggers's avatar
Thom Wiggers committed
223
224


225
226
class MemberGroupMembership(models.Model, metaclass=ModelTranslateMeta):
    """Describes a group membership"""
Thom Wiggers's avatar
Thom Wiggers committed
227
    objects = models.Manager()
228
    active_objects = ActiveMembershipManager()
Thom Wiggers's avatar
Thom Wiggers committed
229
230

    member = models.ForeignKey(
231
        'members.Member',
Thom Wiggers's avatar
Thom Wiggers committed
232
233
234
235
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )

236
    group = models.ForeignKey(
237
        MemberGroup,
Thom Wiggers's avatar
Thom Wiggers committed
238
        on_delete=models.CASCADE,
239
        verbose_name=_('Group'),
Thom Wiggers's avatar
Thom Wiggers committed
240
241
242
    )

    since = models.DateField(
243
244
        verbose_name=_('Member since'),
        help_text=_('The date this member joined in this role'),
Thom Wiggers's avatar
Thom Wiggers committed
245
        default=datetime.date.today
Thom Wiggers's avatar
Thom Wiggers committed
246
247
248
    )

    until = models.DateField(
249
250
        verbose_name=_('Member until'),
        help_text=_("A member until this time "
Thom Wiggers's avatar
Thom Wiggers committed
251
252
253
254
255
256
                    "(can't be in the future)."),
        blank=True,
        null=True,
    )

    chair = models.BooleanField(
257
        verbose_name=_('Chair of the group'),
Thom Wiggers's avatar
Thom Wiggers committed
258
259
260
261
        help_text=_('There can only be one chair at a time!'),
        default=False,
    )

262
263
    role = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
264
265
266
267
268
269
270
        _('role'),
        help_text=_('The role of this member'),
        max_length=255,
        blank=True,
        null=True,
    )

271
272
    @property
    def initial_connected_membership(self):
273
        """Find the oldest membership directly connected to the current one"""
274
275
        qs = MemberGroupMembership.objects.filter(
            group=self.group,
276
277
278
            member=self.member,
            until__lte=self.since,
            until__gte=self.since - datetime.timedelta(days=1))
279
280
281
282
283
        if qs.count() >= 1:  # should actually only be one; should be unique
            return qs.first().initial_connected_membership
        else:
            return self

Thom Wiggers's avatar
Thom Wiggers committed
284
285
286
    @property
    def is_active(self):
        """Is this membership currently active"""
287
        return self.until is None or self.until > timezone.now().date()
Thom Wiggers's avatar
Thom Wiggers committed
288
289
290
291
292

    def clean(self):
        if self.until and (not self.since or self.until < self.since):
            raise ValidationError(
                {'until': _("End date can't be before start date")})
293
294
295
        if self.until and self.until > timezone.now().date():
            raise ValidationError(
                {'until': _("End date can't be in the future")})
296

297
298
        if (self.since and self.group.since and
                self.since < self.group.since):
299
            raise ValidationError(
300
                {'since': _("Start date can't be before group start date")}
301
                )
302
303
        if (self.since and self.group.until and
                self.since > self.group.until):
304
            raise ValidationError(
305
                {'since': _("Start date can't be after group end date")})
306

307
        try:
308
            if self.until and self.group.board:
309
310
311
312
                raise ValidationError(
                    {'until': _("End date cannot be set for boards")})
        except Board.DoesNotExist:
            pass
Thom Wiggers's avatar
Thom Wiggers committed
313
314
315

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
316
        # Check if a group has more than one chair
317
        if self.chair:
318
319
            chairs = (MemberGroupMembership.objects
                      .filter(group=self.group,
320
321
322
323
324
325
                              chair=True))
            for chair in chairs:
                if chair.pk == self.pk:
                    continue
                if ((chair.until is None and
                        (self.until is None or self.until > chair.since)) or
326
327
328
329
                        (self.until is None and self.since < chair.until) or
                        (self.until and chair.until and
                            self.since < chair.until and
                            self.until > chair.since)):
330
331
                    raise ValidationError({
                        NON_FIELD_ERRORS:
332
333
                            _('There already is a '
                              'chair for this time period')})
334

335
        # check if this member is already in the group in this period
336
337
        memberships = (MemberGroupMembership.objects
                       .filter(group=self.group,
338
339
340
341
342
343
                               member=self.member))
        for mship in memberships:
            if mship.pk == self.pk:
                continue
            if ((mship.until is None and
                    (self.until is None or self.until > mship.since)) or
344
345
346
347
                    (self.until is None and self.since < mship.until) or
                    (self.until and mship.until and
                        self.since < mship.until and
                        self.until > mship.since)):
348
                raise ValidationError({
349
                    'member': _('This member is already in the group for '
350
                                'this period')})
351
352
353

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
354
        self.member.is_staff = (self.member
355
                                .membergroupmembership_set
356
357
358
                                .exclude(until__lte=timezone.now().date())
                                .count()) >= 1
        self.member.save()
359

Thom Wiggers's avatar
Thom Wiggers committed
360
    def __str__(self):
361
        return "{} membership of {} since {}, until {}".format(self.member,
362
                                                               self.group,
363
364
                                                               self.since,
                                                               self.until)
Thom Wiggers's avatar
Thom Wiggers committed
365
366

    class Meta:
367
368
        verbose_name = _('group membership')
        verbose_name_plural = _('group memberships')
Joost Rijneveld's avatar
Joost Rijneveld committed
369
370


371
class Mentorship(models.Model):
372
    """Describe a mentorship during the orientation"""
373
    member = models.ForeignKey(
374
        'members.Member',
375
376
377
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )
Thom Wiggers's avatar
Thom Wiggers committed
378
    year = models.IntegerField(validators=[MinValueValidator(1990)])
Joost Rijneveld's avatar
Joost Rijneveld committed
379
380

    def __str__(self):
381
382
383
384
385
        return _("{name} mentor in {year}").format(name=self.member,
                                                   year=self.year)

    class Meta:
        unique_together = ('member', 'year')