models.py 11.1 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
class ActiveMemberGroupsManager(models.Manager):
Thom Wiggers's avatar
Thom Wiggers committed
21
    """Returns active committees only"""
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 committee"""
Thom Wiggers's avatar
Thom Wiggers committed
31

32
33
    objects = models.Manager()
    active_objects = ActiveMemberGroupsManager()
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
120
121
122
123
124
class Committee(MemberGroup):
    wiki_namespace = models.CharField(
        _('Wiki namespace'),
        null=True,
        blank=True,
        max_length=50)
Thom Wiggers's avatar
Thom Wiggers committed
125

126
127
128
129
    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
130
131


132
133
134
135
136
class Society(MemberGroup):
    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
137
138


139
class Board(MemberGroup):
140
    class Meta:
141
142
        verbose_name = _('board')
        verbose_name_plural = _('boards')
143
        ordering = ['-since']
144
145
146
        permissions = (
            ('board_wiki', _("Access the board wiki")),
        )
147

148
149
150
151
    def save(self, *args, **kwargs):
        self.active = True
        super().save(*args, **kwargs)

Thom Wiggers's avatar
Thom Wiggers committed
152
    def get_absolute_url(self):
153
154
        return reverse('activemembers:board', args=[str(self.since.year),
                                                    str(self.until.year)])
155
156
157

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
158
159
160
161
162
163
164
        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
165
166
167
168
                        (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)):
169
170
171
                    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
172

Thom Wiggers's avatar
Thom Wiggers committed
173

Thom Wiggers's avatar
Thom Wiggers committed
174
class ActiveMembershipManager(models.Manager):
175
176
177
    """
    Customs manager that gets the currently active committee memberships
    """
178

Thom Wiggers's avatar
Thom Wiggers committed
179
    def get_queryset(self):
180
        return super().get_queryset().exclude(until__lt=timezone.now().date())
Thom Wiggers's avatar
Thom Wiggers committed
181
182


183
184
class MemberGroupMembership(models.Model, metaclass=ModelTranslateMeta):
    """Describes a group membership"""
Thom Wiggers's avatar
Thom Wiggers committed
185
    objects = models.Manager()
Thom Wiggers's avatar
Thom Wiggers committed
186
    active_memberships = ActiveMembershipManager()
Thom Wiggers's avatar
Thom Wiggers committed
187
188

    member = models.ForeignKey(
189
        'members.Member',
Thom Wiggers's avatar
Thom Wiggers committed
190
191
192
193
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )

194
    group = models.ForeignKey(
195
        MemberGroup,
Thom Wiggers's avatar
Thom Wiggers committed
196
197
198
199
200
        on_delete=models.CASCADE,
        verbose_name=_('Committee'),
    )

    since = models.DateField(
201
202
        verbose_name=_('Member since'),
        help_text=_('The date this member joined in this role'),
Thom Wiggers's avatar
Thom Wiggers committed
203
        default=datetime.date.today
Thom Wiggers's avatar
Thom Wiggers committed
204
205
206
    )

    until = models.DateField(
207
208
        verbose_name=_('Member until'),
        help_text=_("A member until this time "
Thom Wiggers's avatar
Thom Wiggers committed
209
210
211
212
213
214
215
216
217
218
219
                    "(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,
    )

220
221
    role = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
222
223
224
225
226
227
228
        _('role'),
        help_text=_('The role of this member'),
        max_length=255,
        blank=True,
        null=True,
    )

229
230
    @property
    def initial_connected_membership(self):
231
        """Find the oldest membership directly connected to the current one"""
232
233
        qs = MemberGroupMembership.objects.filter(
            group=self.group,
234
235
236
            member=self.member,
            until__lte=self.since,
            until__gte=self.since - datetime.timedelta(days=1))
237
238
239
240
241
        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
242
243
244
    @property
    def is_active(self):
        """Is this membership currently active"""
245
        return self.until is None or self.until > timezone.now().date()
Thom Wiggers's avatar
Thom Wiggers committed
246
247
248
249
250

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

255
256
        if (self.since and self.group.since and
                self.since < self.group.since):
257
258
259
            raise ValidationError(
                {'since': _("Start date can't be before committee start date")}
                )
260
261
        if (self.since and self.group.until and
                self.since > self.group.until):
262
263
264
            raise ValidationError(
                {'since': _("Start date can't be after committee end date")})

265
        try:
266
            if self.until and self.group.board:
267
268
269
270
                raise ValidationError(
                    {'until': _("End date cannot be set for boards")})
        except Board.DoesNotExist:
            pass
Thom Wiggers's avatar
Thom Wiggers committed
271
272
273
274

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
        # Check if a committee has more than one chair
275
        if self.chair:
276
277
            chairs = (MemberGroupMembership.objects
                      .filter(group=self.group,
278
279
280
281
282
283
                              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
284
285
286
287
                        (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)):
288
289
                    raise ValidationError({
                        NON_FIELD_ERRORS:
290
291
                            _('There already is a '
                              'chair for this time period')})
292
293

        # check if this member is already in the committee in this period
294
295
        memberships = (MemberGroupMembership.objects
                       .filter(group=self.group,
296
297
298
299
300
301
                               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
302
303
304
305
                    (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)):
306
                raise ValidationError({
307
                    'member': _('This member is already in the group for '
308
                                'this period')})
309
310
311

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
312
        self.member.is_staff = (self.member
313
                                .membergroupmembership_set
314
315
316
                                .exclude(until__lte=timezone.now().date())
                                .count()) >= 1
        self.member.save()
317

Thom Wiggers's avatar
Thom Wiggers committed
318
    def __str__(self):
319
        return "{} membership of {} since {}, until {}".format(self.member,
320
                                                               self.group,
321
322
                                                               self.since,
                                                               self.until)
Thom Wiggers's avatar
Thom Wiggers committed
323
324

    class Meta:
325
326
        verbose_name = _('group membership')
        verbose_name_plural = _('group memberships')
Joost Rijneveld's avatar
Joost Rijneveld committed
327
328


329
class Mentorship(models.Model):
330
    """Describe a mentorship during the orientation"""
331
    member = models.ForeignKey(
332
        'members.Member',
333
334
335
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )
Thom Wiggers's avatar
Thom Wiggers committed
336
    year = models.IntegerField(validators=[MinValueValidator(1990)])
Joost Rijneveld's avatar
Joost Rijneveld committed
337
338

    def __str__(self):
339
340
341
342
343
        return _("{name} mentor in {year}").format(name=self.member,
                                                   year=self.year)

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