models.py 12 KB
Newer Older
Thom Wiggers's avatar
Thom Wiggers committed
1
import datetime
2
import logging
Thom Wiggers's avatar
Thom Wiggers committed
3

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

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

16
17
18
logger = logging.getLogger(__name__)


19
20
class UnfilteredSortedManager(models.Manager):
    """Returns committees and boards, sorted by name"""
21

22
23
24
25
26
    def get_queryset(self):
        return (super().get_queryset()
                .order_by(localize_attr_name('name')))


Thom Wiggers's avatar
Thom Wiggers committed
27
28
class CommitteeManager(models.Manager):
    """Returns committees only"""
29

Thom Wiggers's avatar
Thom Wiggers committed
30
31
    def get_queryset(self):
        return (super().get_queryset()
32
33
                .exclude(board__is_board=True)
                .order_by(localize_attr_name('name')))
Thom Wiggers's avatar
Thom Wiggers committed
34
35
36


class ActiveCommitteeManager(models.Manager):
Thom Wiggers's avatar
Thom Wiggers committed
37
    """Returns active committees only"""
38

Thom Wiggers's avatar
Thom Wiggers committed
39
    def get_queryset(self):
Thom Wiggers's avatar
Thom Wiggers committed
40
        return (super().get_queryset()
41
                .exclude(board__is_board=True)
42
43
                .exclude(active=False)
                .order_by(localize_attr_name('name')))
Thom Wiggers's avatar
Thom Wiggers committed
44
45


46
class Committee(models.Model, metaclass=ModelTranslateMeta):
Thom Wiggers's avatar
Thom Wiggers committed
47
48
    """A committee"""

49
    unfiltered_objects = UnfilteredSortedManager()
Thom Wiggers's avatar
Thom Wiggers committed
50
    objects = CommitteeManager()
51
    active_committees = ActiveCommitteeManager()
Thom Wiggers's avatar
Thom Wiggers committed
52

53
54
    name = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
55
56
57
58
59
        max_length=40,
        verbose_name=_('Committee name'),
        unique=True,
    )

60
    description = MultilingualField(
61
        HTMLField,
Thom Wiggers's avatar
Thom Wiggers committed
62
63
64
65
66
        verbose_name=_('Description'),
    )

    photo = models.ImageField(
        verbose_name=_('Image'),
Thom Wiggers's avatar
Thom Wiggers committed
67
        upload_to='public/committeephotos/',
Thom Wiggers's avatar
Thom Wiggers committed
68
69
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
70
71
72
    )

    members = models.ManyToManyField(
73
        'members.Member',
Thom Wiggers's avatar
Thom Wiggers committed
74
75
76
77
78
79
80
81
82
        through='CommitteeMembership'
    )

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

Thom Wiggers's avatar
Thom Wiggers committed
83
84
85
86
87
    since = models.DateField(
        _('founded in'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
88

Thom Wiggers's avatar
Thom Wiggers committed
89
90
91
92
93
    until = models.DateField(
        _('existed until'),
        null=True,
        blank=True,
    )
Thom Wiggers's avatar
Thom Wiggers committed
94

95
96
    active = models.BooleanField(default=False)

97
98
99
100
101
102
    contact_email = models.EmailField(
        _('contact email address'),
        blank=True,
        null=True,
    )

103
104
    contact_mailinglist = models.OneToOneField(
        'mailinglists.MailingList',
105
        verbose_name=_('contact mailing list'),
106
107
        null=True,
        blank=True,
Thom Wiggers's avatar
Thom Wiggers committed
108
        on_delete=models.SET_NULL,
109
    )
Thom Wiggers's avatar
Thom Wiggers committed
110

Thom Wiggers's avatar
Thom Wiggers committed
111
112
113
114
115
116
    wiki_namespace = models.CharField(
        _('Wiki namespace'),
        null=True,
        blank=True,
        max_length=50)

117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
    def clean(self):
        """Validation"""
        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
132
133
134
    def __str__(self):
        return self.name

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

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


Thom Wiggers's avatar
Thom Wiggers committed
144
class BoardManager(models.Manager):
Thom Wiggers's avatar
Thom Wiggers committed
145
146
147

    use_in_migrations = True

Thom Wiggers's avatar
Thom Wiggers committed
148
    def get_queryset(self):
149
150
        # sorting by descending order by default makes more sense for boards
        return (super().get_queryset()
151
152
                .filter(is_board=True)
                .order_by(localize_attr_name('-name')))
Thom Wiggers's avatar
Thom Wiggers committed
153
154
155


class Board(Committee):
156
157
158
159
160
    """ Because Board inherits from Committee, Django creates a OneToOneField
    linking the two models together. This can be accessed as usual;
    given a Committee or Board b, one can access b.board, which will either
    return the object b if b is a Board, or a Board.DoesNotExist exception.
    """
Thom Wiggers's avatar
Thom Wiggers committed
161
162
163
164
165
166
167
    objects = BoardManager()

    is_board = models.BooleanField(
        verbose_name=_('Is this a board'),
        default=True,
    )

168
169
    class Meta:
        ordering = ['-since']
170
171
172
        permissions = (
            ('board_wiki', _("Access the board wiki")),
        )
173

174
175
176
177
    def save(self, *args, **kwargs):
        self.active = True
        super().save(*args, **kwargs)

Thom Wiggers's avatar
Thom Wiggers committed
178
    def get_absolute_url(self):
179
180
        return reverse('activemembers:board', args=[str(self.since.year),
                                                    str(self.until.year)])
181
182
183
184

    def validate_unique(self, *args, **kwargs):
        """ Check uniqueness"""
        super().validate_unique(*args, **kwargs)
185
186
187
188
189
190
191
        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
192
193
194
195
                        (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)):
196
197
198
                    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
199

Thom Wiggers's avatar
Thom Wiggers committed
200

Thom Wiggers's avatar
Thom Wiggers committed
201
202
class ActiveMembershipManager(models.Manager):
    """Get only active memberships"""
203

Thom Wiggers's avatar
Thom Wiggers committed
204
205
    def get_queryset(self):
        """Get the currently active committee memberships"""
206
        return super().get_queryset().exclude(until__lt=timezone.now().date())
Thom Wiggers's avatar
Thom Wiggers committed
207
208


209
class CommitteeMembership(models.Model, metaclass=ModelTranslateMeta):
Thom Wiggers's avatar
Thom Wiggers committed
210
    objects = models.Manager()
Thom Wiggers's avatar
Thom Wiggers committed
211
    active_memberships = ActiveMembershipManager()
Thom Wiggers's avatar
Thom Wiggers committed
212
213

    member = models.ForeignKey(
214
        'members.Member',
Thom Wiggers's avatar
Thom Wiggers committed
215
216
217
218
219
220
221
222
223
224
225
226
227
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )

    committee = models.ForeignKey(
        Committee,
        on_delete=models.CASCADE,
        verbose_name=_('Committee'),
    )

    since = models.DateField(
        verbose_name=_('Committee member since'),
        help_text=_('The date this member joined the committee in this role'),
Thom Wiggers's avatar
Thom Wiggers committed
228
        default=datetime.date.today
Thom Wiggers's avatar
Thom Wiggers committed
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
    )

    until = models.DateField(
        verbose_name=_('Committee member until'),
        help_text=_("A member of this committee until this time "
                    "(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,
    )

245
246
    role = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
247
248
249
250
251
252
253
        _('role'),
        help_text=_('The role of this member'),
        max_length=255,
        blank=True,
        null=True,
    )

254
255
256
    @property
    def initial_connected_membership(self):
        """ Find the oldest membership directly connected to the current one"""
257
258
259
260
261
        qs = CommitteeMembership.objects.filter(
            committee=self.committee,
            member=self.member,
            until__lte=self.since,
            until__gte=self.since - datetime.timedelta(days=1))
262
263
264
265
266
        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
267
268
269
    @property
    def is_active(self):
        """Is this membership currently active"""
270
        return self.until is None or self.until > timezone.now().date()
Thom Wiggers's avatar
Thom Wiggers committed
271
272
273
274
275
276

    def clean(self):
        """Validation"""
        if self.until and (not self.since or self.until < self.since):
            raise ValidationError(
                {'until': _("End date can't be before start date")})
277
278
279
        if self.until and self.until > timezone.now().date():
            raise ValidationError(
                {'until': _("End date can't be in the future")})
280
281
282
283
284
285
286
287
288
289
290

        if (self.since and self.committee.since and
                self.since < self.committee.since):
            raise ValidationError(
                {'since': _("Start date can't be before committee start date")}
                )
        if (self.since and self.committee.until and
                self.since > self.committee.until):
            raise ValidationError(
                {'since': _("Start date can't be after committee end date")})

291
292
293
294
295
296
        try:
            if self.until and self.committee.board:
                raise ValidationError(
                    {'until': _("End date cannot be set for boards")})
        except Board.DoesNotExist:
            pass
Thom Wiggers's avatar
Thom Wiggers committed
297
298
299
300
301

    def validate_unique(self, *args, **kwargs):
        """ Check uniqueness"""
        super().validate_unique(*args, **kwargs)
        # Check if a committee has more than one chair
302
303
304
305
306
307
308
309
310
        if self.chair:
            chairs = (CommitteeMembership.objects
                      .filter(committee=self.committee,
                              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
311
312
313
314
                        (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)):
315
316
                    raise ValidationError({
                        NON_FIELD_ERRORS:
317
318
                            _('There already is a '
                              'chair for this time period')})
319
320
321
322
323
324
325
326
327
328

        # check if this member is already in the committee in this period
        memberships = (CommitteeMembership.objects
                       .filter(committee=self.committee,
                               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
329
330
331
332
                    (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)):
333
334
335
                raise ValidationError({
                    'member': _('This member is already in the committee for '
                                'this period')})
336
337
338

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
339
340
341
342
343
        self.member.is_staff = (self.member
                                .committeemembership_set
                                .exclude(until__lte=timezone.now().date())
                                .count()) >= 1
        self.member.save()
344

Thom Wiggers's avatar
Thom Wiggers committed
345
    def __str__(self):
346
347
348
349
        return "{} membership of {} since {}, until {}".format(self.member,
                                                               self.committee,
                                                               self.since,
                                                               self.until)
Thom Wiggers's avatar
Thom Wiggers committed
350
351
352
353

    class Meta:
        verbose_name = _('committee membership')
        verbose_name_plural = _('committee memberships')
Joost Rijneveld's avatar
Joost Rijneveld committed
354
355


356
class Mentorship(models.Model):
357
    member = models.ForeignKey(
358
        'members.Member',
359
360
361
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )
Thom Wiggers's avatar
Thom Wiggers committed
362
    year = models.IntegerField(validators=[MinValueValidator(1990)])
Joost Rijneveld's avatar
Joost Rijneveld committed
363
364

    def __str__(self):
365
366
367
368
369
        return _("{name} mentor in {year}").format(name=self.member,
                                                   year=self.year)

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