models.py 11.5 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
38
39
40
41
        max_length=40,
        verbose_name=_('Committee name'),
        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
    active = models.BooleanField(default=False)

79
80
81
82
83
84
    contact_email = models.EmailField(
        _('contact email address'),
        blank=True,
        null=True,
    )

85
86
    contact_mailinglist = models.OneToOneField(
        'mailinglists.MailingList',
87
        verbose_name=_('contact mailing list'),
88
89
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
90
        on_delete=models.SET_NULL,
91
    )
Thom Wiggers's avatar
Thom Wiggers committed
92

93
94
95
96
97
98
99
100
101
102
103
104
105
106
    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
107
108
109
    def __str__(self):
        return self.name

Thom Wiggers's avatar
Thom Wiggers committed
110
    def get_absolute_url(self):
111
        return reverse('activemembers:committee', args=[str(self.pk)])
Thom Wiggers's avatar
Thom Wiggers committed
112

Thom Wiggers's avatar
Thom Wiggers committed
113
    class Meta:
114
115
        verbose_name = _('member group')
        verbose_name_plural = _('member groups')
116
        # ordering is done in the manager, to sort on a translated field
Thom Wiggers's avatar
Thom Wiggers committed
117
118


119
class Committee(MemberGroup):
120
121
122
123
124
    """Describes a committee, which is a type of MemberGroup"""

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

125
126
127
128
129
    wiki_namespace = models.CharField(
        _('Wiki namespace'),
        null=True,
        blank=True,
        max_length=50)
Thom Wiggers's avatar
Thom Wiggers committed
130

131
132
133
134
    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
135
136


137
class Society(MemberGroup):
138
139
140
141
142
    """Describes a society, which is a type of MemberGroup"""

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

143
144
145
146
    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
147
148


149
class Board(MemberGroup):
150
151
    """Describes a board, which is a type of MemberGroup"""

152
    class Meta:
153
154
        verbose_name = _('board')
        verbose_name_plural = _('boards')
155
        ordering = ['-since']
156
157
158
        permissions = (
            ('board_wiki', _("Access the board wiki")),
        )
159

160
161
162
163
    def save(self, *args, **kwargs):
        self.active = True
        super().save(*args, **kwargs)

Thom Wiggers's avatar
Thom Wiggers committed
164
    def get_absolute_url(self):
165
166
        return reverse('activemembers:board', args=[str(self.since.year),
                                                    str(self.until.year)])
167
168
169

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
170
171
172
173
174
175
176
        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
177
178
179
180
                        (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)):
181
182
183
                    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
184

Thom Wiggers's avatar
Thom Wiggers committed
185

Thom Wiggers's avatar
Thom Wiggers committed
186
class ActiveMembershipManager(models.Manager):
187
    """
188
    Custom manager that gets the currently active membergroup memberships
189
    """
190

Thom Wiggers's avatar
Thom Wiggers committed
191
    def get_queryset(self):
192
        return super().get_queryset().exclude(until__lt=timezone.now().date())
Thom Wiggers's avatar
Thom Wiggers committed
193
194


195
196
class MemberGroupMembership(models.Model, metaclass=ModelTranslateMeta):
    """Describes a group membership"""
Thom Wiggers's avatar
Thom Wiggers committed
197
    objects = models.Manager()
198
    active_objects = ActiveMembershipManager()
Thom Wiggers's avatar
Thom Wiggers committed
199
200

    member = models.ForeignKey(
201
        'members.Member',
Thom Wiggers's avatar
Thom Wiggers committed
202
203
204
205
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )

206
    group = models.ForeignKey(
207
        MemberGroup,
Thom Wiggers's avatar
Thom Wiggers committed
208
209
210
211
212
        on_delete=models.CASCADE,
        verbose_name=_('Committee'),
    )

    since = models.DateField(
213
214
        verbose_name=_('Member since'),
        help_text=_('The date this member joined in this role'),
Thom Wiggers's avatar
Thom Wiggers committed
215
        default=datetime.date.today
Thom Wiggers's avatar
Thom Wiggers committed
216
217
218
    )

    until = models.DateField(
219
220
        verbose_name=_('Member until'),
        help_text=_("A member until this time "
Thom Wiggers's avatar
Thom Wiggers committed
221
222
223
224
225
226
227
228
229
230
231
                    "(can't be in the future)."),
        blank=True,
        null=True,
    )

    chair = models.BooleanField(
        verbose_name=_('Chair of the committee'),
        help_text=_('There can only be one chair at a time!'),
        default=False,
    )

232
233
    role = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
234
235
236
237
238
239
240
        _('role'),
        help_text=_('The role of this member'),
        max_length=255,
        blank=True,
        null=True,
    )

241
242
    @property
    def initial_connected_membership(self):
243
        """Find the oldest membership directly connected to the current one"""
244
245
        qs = MemberGroupMembership.objects.filter(
            group=self.group,
246
247
248
            member=self.member,
            until__lte=self.since,
            until__gte=self.since - datetime.timedelta(days=1))
249
250
251
252
253
        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
254
255
256
    @property
    def is_active(self):
        """Is this membership currently active"""
257
        return self.until is None or self.until > timezone.now().date()
Thom Wiggers's avatar
Thom Wiggers committed
258
259
260
261
262

    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")})
263
264
265
        if self.until and self.until > timezone.now().date():
            raise ValidationError(
                {'until': _("End date can't be in the future")})
266

267
268
        if (self.since and self.group.since and
                self.since < self.group.since):
269
270
271
            raise ValidationError(
                {'since': _("Start date can't be before committee start date")}
                )
272
273
        if (self.since and self.group.until and
                self.since > self.group.until):
274
275
276
            raise ValidationError(
                {'since': _("Start date can't be after committee end date")})

277
        try:
278
            if self.until and self.group.board:
279
280
281
282
                raise ValidationError(
                    {'until': _("End date cannot be set for boards")})
        except Board.DoesNotExist:
            pass
Thom Wiggers's avatar
Thom Wiggers committed
283
284
285
286

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
        # Check if a committee has more than one chair
287
        if self.chair:
288
289
            chairs = (MemberGroupMembership.objects
                      .filter(group=self.group,
290
291
292
293
294
295
                              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
296
297
298
299
                        (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)):
300
301
                    raise ValidationError({
                        NON_FIELD_ERRORS:
302
303
                            _('There already is a '
                              'chair for this time period')})
304
305

        # check if this member is already in the committee in this period
306
307
        memberships = (MemberGroupMembership.objects
                       .filter(group=self.group,
308
309
310
311
312
313
                               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
314
315
316
317
                    (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)):
318
                raise ValidationError({
319
                    'member': _('This member is already in the group for '
320
                                'this period')})
321
322
323

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
324
        self.member.is_staff = (self.member
325
                                .membergroupmembership_set
326
327
328
                                .exclude(until__lte=timezone.now().date())
                                .count()) >= 1
        self.member.save()
329

Thom Wiggers's avatar
Thom Wiggers committed
330
    def __str__(self):
331
        return "{} membership of {} since {}, until {}".format(self.member,
332
                                                               self.group,
333
334
                                                               self.since,
                                                               self.until)
Thom Wiggers's avatar
Thom Wiggers committed
335
336

    class Meta:
337
338
        verbose_name = _('group membership')
        verbose_name_plural = _('group memberships')
Joost Rijneveld's avatar
Joost Rijneveld committed
339
340


341
class Mentorship(models.Model):
342
    """Describe a mentorship during the orientation"""
343
    member = models.ForeignKey(
344
        'members.Member',
345
346
347
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )
Thom Wiggers's avatar
Thom Wiggers committed
348
    year = models.IntegerField(validators=[MinValueValidator(1990)])
Joost Rijneveld's avatar
Joost Rijneveld committed
349
350

    def __str__(self):
351
352
353
354
355
        return _("{name} mentor in {year}").format(name=self.member,
                                                   year=self.year)

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