models.py 11.9 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

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

15
16
17
logger = logging.getLogger(__name__)


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

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


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

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


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

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


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

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

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

59
60
    description = MultilingualField(
        models.TextField,
Thom Wiggers's avatar
Thom Wiggers committed
61
62
63
64
65
        verbose_name=_('Description'),
    )

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

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

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

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

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

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

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

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

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

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

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

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


Thom Wiggers's avatar
Thom Wiggers committed
142
143
class BoardManager(models.Manager):
    def get_queryset(self):
144
145
        # sorting by descending order by default makes more sense for boards
        return (super().get_queryset()
146
147
                .filter(is_board=True)
                .order_by(localize_attr_name('-name')))
Thom Wiggers's avatar
Thom Wiggers committed
148
149
150


class Board(Committee):
151
152
153
154
155
    """ 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
156
157
158
159
160
161
162
    objects = BoardManager()

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

163
164
    class Meta:
        ordering = ['-since']
165
166
167
        permissions = (
            ('board_wiki', _("Access the board wiki")),
        )
168

169
170
171
172
    def save(self, *args, **kwargs):
        self.active = True
        super().save(*args, **kwargs)

Thom Wiggers's avatar
Thom Wiggers committed
173
    def get_absolute_url(self):
174
175
        return reverse('activemembers:board', args=[str(self.since.year),
                                                    str(self.until.year)])
176
177
178
179

    def validate_unique(self, *args, **kwargs):
        """ Check uniqueness"""
        super().validate_unique(*args, **kwargs)
180
181
182
183
184
185
186
        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
187
188
189
190
                        (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)):
191
192
193
                    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
194

Thom Wiggers's avatar
Thom Wiggers committed
195

Thom Wiggers's avatar
Thom Wiggers committed
196
197
class ActiveMembershipManager(models.Manager):
    """Get only active memberships"""
198

Thom Wiggers's avatar
Thom Wiggers committed
199
200
    def get_queryset(self):
        """Get the currently active committee memberships"""
201
        return super().get_queryset().exclude(until__lt=timezone.now().date())
Thom Wiggers's avatar
Thom Wiggers committed
202
203


204
class CommitteeMembership(models.Model, metaclass=ModelTranslateMeta):
Thom Wiggers's avatar
Thom Wiggers committed
205
    objects = models.Manager()
Thom Wiggers's avatar
Thom Wiggers committed
206
    active_memberships = ActiveMembershipManager()
Thom Wiggers's avatar
Thom Wiggers committed
207
208

    member = models.ForeignKey(
209
        'members.Member',
Thom Wiggers's avatar
Thom Wiggers committed
210
211
212
213
214
215
216
217
218
219
220
221
222
        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
223
        default=datetime.date.today
Thom Wiggers's avatar
Thom Wiggers committed
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
    )

    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,
    )

240
241
    role = MultilingualField(
        models.CharField,
Thom Wiggers's avatar
Thom Wiggers committed
242
243
244
245
246
247
248
        _('role'),
        help_text=_('The role of this member'),
        max_length=255,
        blank=True,
        null=True,
    )

249
250
251
252
253
254
255
256
257
258
259
    @property
    def initial_connected_membership(self):
        """ Find the oldest membership directly connected to the current one"""
        qs = CommitteeMembership.objects.filter(committee=self.committee,
                                                member=self.member,
                                                until=self.since)
        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
260
261
262
    @property
    def is_active(self):
        """Is this membership currently active"""
263
        return self.until is None or self.until > timezone.now().date()
Thom Wiggers's avatar
Thom Wiggers committed
264
265
266
267
268
269

    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")})
270
271
272
        if self.until and self.until > timezone.now().date():
            raise ValidationError(
                {'until': _("End date can't be in the future")})
273
274
275
276
277
278
279
280
281
282
283

        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")})

284
285
286
287
288
289
        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
290
291
292
293
294

    def validate_unique(self, *args, **kwargs):
        """ Check uniqueness"""
        super().validate_unique(*args, **kwargs)
        # Check if a committee has more than one chair
295
296
297
298
299
300
301
302
303
        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
304
305
306
307
                        (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)):
308
309
                    raise ValidationError({
                        NON_FIELD_ERRORS:
310
311
                            _('There already is a '
                              'chair for this time period')})
312
313
314
315
316
317
318
319
320
321

        # 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
322
323
324
325
                    (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)):
326
327
328
                raise ValidationError({
                    'member': _('This member is already in the committee for '
                                'this period')})
329
330
331

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
332
333
334
335
336
        self.member.is_staff = (self.member
                                .committeemembership_set
                                .exclude(until__lte=timezone.now().date())
                                .count()) >= 1
        self.member.save()
337

Thom Wiggers's avatar
Thom Wiggers committed
338
    def __str__(self):
339
340
341
342
        return "{} membership of {} since {}, until {}".format(self.member,
                                                               self.committee,
                                                               self.since,
                                                               self.until)
Thom Wiggers's avatar
Thom Wiggers committed
343
344
345
346

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


349
class Mentorship(models.Model):
350
    member = models.ForeignKey(
351
        'members.Member',
352
353
354
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
    )
Thom Wiggers's avatar
Thom Wiggers committed
355
    year = models.IntegerField(validators=[MinValueValidator(1990)])
Joost Rijneveld's avatar
Joost Rijneveld committed
356
357

    def __str__(self):
358
359
360
361
362
        return _("{name} mentor in {year}").format(name=self.member,
                                                   year=self.year)

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